Repository: getmoto/moto
Pull request #8840 (2025-04-21 22:52:52)

Bump ruby/setup-ruby from 1.230.0 to 1.233.0
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.230.0 to 1.233.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/ruby/setup-ruby/releases">ruby/setup-ruby's releases</a>.</em></p>
<blockquote>
<h2>v1.233.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update remaining 20.04 tests to 22.04 by <a href="https://github.com/eregon"><code>@​eregon</code></a> in <a href="https://redirect.github.com/ruby/setup-ruby/pull/747">ruby/setup-ruby#747</a></li>
<li>Add ruby-3.5.0-preview1 by <a href="https://github.com/ruby-builder-bot"><code>@​ruby-builder-bot</code></a> in <a href="https://redirect.github.com/ruby/setup-ruby/pull/746">ruby/setup-ruby#746</a></li>
<li>Add jruby-10.0.0.0 by <a href="https://github.com/ruby-builder-bot"><code>@​ruby-builder-bot</code></a> in <a href="https://redirect.github.com/ruby/setup-ruby/pull/745">ruby/setup-ruby#745</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/ruby/setup-ruby/compare/v1.232.0...v1.233.0">https://github.com/ruby/setup-ruby/compare/v1.232.0...v1.233.0</a></p>
<h2>v1.232.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update CRuby releases on Windows by <a href="https://github.com/ruby-builder-bot"><code>@​ruby-builder-bot</code></a> in <a href="https://redirect.github.com/ruby/setup-ruby/pull/743">ruby/setup-ruby#743</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/ruby/setup-ruby/compare/v1.231.0...v1.232.0">https://github.com/ruby/setup-ruby/compare/v1.231.0...v1.232.0</a></p>
<h2>v1.231.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Add ruby-3.4.3 by <a href="https://github.com/ruby-builder-bot"><code>@​ruby-builder-bot</code></a> in <a href="https://redirect.github.com/ruby/setup-ruby/pull/740">ruby/setup-ruby#740</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/ruby/setup-ruby/compare/v1.230.0...v1.231.0">https://github.com/ruby/setup-ruby/compare/v1.230.0...v1.231.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/ruby/setup-ruby/commit/ca041f971d66735f3e5ff1e21cc13e2d51e7e535"><code>ca041f9</code></a> Add jruby-10.0.0.0</li>
<li><a href="https://github.com/ruby/setup-ruby/commit/1eb8bd2dc10a2e212c665d1435972b2e51163e10"><code>1eb8bd2</code></a> Add ruby-3.5.0-preview1</li>
<li><a href="https://github.com/ruby/setup-ruby/commit/fc67f6de0a89e8c33be7887fe182fa1fe47dda3c"><code>fc67f6d</code></a> Update remaining 20.04 tests to 22.04</li>
<li><a href="https://github.com/ruby/setup-ruby/commit/5ea2b47b47ae4e5f5dbb0fae152979bf8514be6c"><code>5ea2b47</code></a> Stop testing on ubuntu-20.04 since it's gone</li>
<li><a href="https://github.com/ruby/setup-ruby/commit/fb404b9557c186e349162b0d8efb06e2bc36edea"><code>fb404b9</code></a> Update CRuby releases on Windows</li>
<li><a href="https://github.com/ruby/setup-ruby/commit/d8d83c3960843afb664e821fed6be52f37da5267"><code>d8d83c3</code></a> Add ruby-3.4.3</li>
<li>See full diff in <a href="https://github.com/ruby/setup-ruby/compare/e5ac7b085f6e63d49c8973eb0c6e04d876b881f1...ca041f971d66735f3e5ff1e21cc13e2d51e7e535">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ruby/setup-ruby&package-manager=github_actions&previous-version=1.230.0&new-version=1.233.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index b255bfd4e..e3005e1c2 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -51,7 +51,7 @@
 
 ## amp
 <details>
-<summary>58% implemented</summary>
+<summary>62% implemented</summary>
 
 - [ ] create_alert_manager_definition
 - [X] create_logging_configuration
@@ -68,7 +68,6 @@
 - [X] describe_rule_groups_namespace
 - [ ] describe_scraper
 - [X] describe_workspace
-- [ ] describe_workspace_configuration
 - [ ] get_default_scraper_configuration
 - [X] list_rule_groups_namespaces
 - [ ] list_scrapers
@@ -81,7 +80,6 @@
 - [X] update_logging_configuration
 - [ ] update_scraper
 - [X] update_workspace_alias
-- [ ] update_workspace_configuration
 </details>
 
 ## apigateway
@@ -2389,14 +2387,13 @@
 
 ## dsql
 <details>
-<summary>18% implemented</summary>
+<summary>20% implemented</summary>
 
 - [X] create_cluster
 - [ ] create_multi_region_clusters
 - [ ] delete_cluster
 - [ ] delete_multi_region_clusters
 - [X] get_cluster
-- [ ] get_vpc_endpoint_service_name
 - [ ] list_clusters
 - [ ] list_tags_for_resource
 - [ ] tag_resource
@@ -8036,7 +8033,7 @@
 
 ## s3tables
 <details>
-<summary>46% implemented</summary>
+<summary>53% implemented</summary>
 
 - [X] create_namespace
 - [X] create_table
@@ -8044,16 +8041,13 @@
 - [X] delete_namespace
 - [X] delete_table
 - [X] delete_table_bucket
-- [ ] delete_table_bucket_encryption
 - [ ] delete_table_bucket_policy
 - [ ] delete_table_policy
 - [X] get_namespace
 - [X] get_table
 - [X] get_table_bucket
-- [ ] get_table_bucket_encryption
 - [ ] get_table_bucket_maintenance_configuration
 - [ ] get_table_bucket_policy
-- [ ] get_table_encryption
 - [ ] get_table_maintenance_configuration
 - [ ] get_table_maintenance_job_status
 - [ ] get_table_metadata_location
@@ -8061,7 +8055,6 @@
 - [X] list_namespaces
 - [X] list_table_buckets
 - [X] list_tables
-- [ ] put_table_bucket_encryption
 - [ ] put_table_bucket_maintenance_configuration
 - [ ] put_table_bucket_policy
 - [ ] put_table_maintenance_configuration
@@ -9467,20 +9460,20 @@
 
 ## timestream-influxdb
 <details>
-<summary>41% implemented</summary>
+<summary>76% implemented</summary>
 
-- [ ] create_db_cluster
+- [X] create_db_cluster
 - [X] create_db_instance
-- [ ] create_db_parameter_group
+- [X] create_db_parameter_group
 - [ ] delete_db_cluster
 - [X] delete_db_instance
-- [ ] get_db_cluster
+- [X] get_db_cluster
 - [X] get_db_instance
-- [ ] get_db_parameter_group
-- [ ] list_db_clusters
+- [X] get_db_parameter_group
+- [X] list_db_clusters
 - [X] list_db_instances
 - [ ] list_db_instances_for_cluster
-- [ ] list_db_parameter_groups
+- [X] list_db_parameter_groups
 - [X] list_tags_for_resource
 - [X] tag_resource
 - [X] untag_resource
diff --git a/docs/docs/services/amp.rst b/docs/docs/services/amp.rst
index 471e3ec1a..016e4dbfb 100644
--- a/docs/docs/services/amp.rst
+++ b/docs/docs/services/amp.rst
@@ -47,7 +47,6 @@ amp
 - [X] describe_rule_groups_namespace
 - [ ] describe_scraper
 - [X] describe_workspace
-- [ ] describe_workspace_configuration
 - [ ] get_default_scraper_configuration
 - [X] list_rule_groups_namespaces
 - [ ] list_scrapers
@@ -68,5 +67,4 @@ amp
         The ClientToken-parameter is not yet implemented
         
 
-- [ ] update_workspace_configuration
 
diff --git a/docs/docs/services/dsql.rst b/docs/docs/services/dsql.rst
index 06a589c17..02bdadf65 100644
--- a/docs/docs/services/dsql.rst
+++ b/docs/docs/services/dsql.rst
@@ -21,7 +21,6 @@ dsql
 - [ ] delete_cluster
 - [ ] delete_multi_region_clusters
 - [X] get_cluster
-- [ ] get_vpc_endpoint_service_name
 - [ ] list_clusters
 - [ ] list_tags_for_resource
 - [ ] tag_resource
diff --git a/docs/docs/services/s3tables.rst b/docs/docs/services/s3tables.rst
index 03e0a72fd..4ad3d48ca 100644
--- a/docs/docs/services/s3tables.rst
+++ b/docs/docs/services/s3tables.rst
@@ -22,16 +22,13 @@ s3tables
 - [X] delete_namespace
 - [X] delete_table
 - [X] delete_table_bucket
-- [ ] delete_table_bucket_encryption
 - [ ] delete_table_bucket_policy
 - [ ] delete_table_policy
 - [X] get_namespace
 - [X] get_table
 - [X] get_table_bucket
-- [ ] get_table_bucket_encryption
 - [ ] get_table_bucket_maintenance_configuration
 - [ ] get_table_bucket_policy
-- [ ] get_table_encryption
 - [ ] get_table_maintenance_configuration
 - [ ] get_table_maintenance_job_status
 - [ ] get_table_metadata_location
@@ -39,7 +36,6 @@ s3tables
 - [X] list_namespaces
 - [X] list_table_buckets
 - [X] list_tables
-- [ ] put_table_bucket_encryption
 - [ ] put_table_bucket_maintenance_configuration
 - [ ] put_table_bucket_policy
 - [ ] put_table_maintenance_configuration
diff --git a/docs/docs/services/timestream-influxdb.rst b/docs/docs/services/timestream-influxdb.rst
index 4b0cf8300..284585aea 100644
--- a/docs/docs/services/timestream-influxdb.rst
+++ b/docs/docs/services/timestream-influxdb.rst
@@ -16,7 +16,7 @@ timestream-influxdb
 
 |start-h3| Implemented features for this service |end-h3|
 
-- [ ] create_db_cluster
+- [X] create_db_cluster
 - [X] create_db_instance
   
         dbParameterGroupIdentifier argument is not yet handled
@@ -27,20 +27,20 @@ timestream-influxdb
         influxAuthParametersSecretArn is not yet handled
         
 
-- [ ] create_db_parameter_group
+- [X] create_db_parameter_group
 - [ ] delete_db_cluster
 - [X] delete_db_instance
-- [ ] get_db_cluster
+- [X] get_db_cluster
 - [X] get_db_instance
-- [ ] get_db_parameter_group
-- [ ] list_db_clusters
+- [X] get_db_parameter_group
+- [X] list_db_clusters
 - [X] list_db_instances
   
         Pagination is not yet implemented
         
 
 - [ ] list_db_instances_for_cluster
-- [ ] list_db_parameter_groups
+- [X] list_db_parameter_groups
 - [X] list_tags_for_resource
 - [X] tag_resource
 - [X] untag_resource
diff --git a/moto/backend_index.py b/moto/backend_index.py
index b65523732..cb12ebb24 100644
--- a/moto/backend_index.py
+++ b/moto/backend_index.py
@@ -196,6 +196,10 @@ backend_url_patterns = [
     ("secretsmanager", re.compile("https?://secretsmanager\\.(.+)\\.amazonaws\\.com")),
     ("securityhub", re.compile("https?://securityhub\\.(.+)\\.amazonaws\\.com")),
     ("servicecatalog", re.compile("https?://servicecatalog\\.(.+)\\.amazonaws\\.com")),
+    (
+        "servicecatalogappregistry",
+        re.compile("https?://servicecatalog-appregistry\\.(.+)\\.amazonaws\\.com"),
+    ),
     (
         "servicediscovery",
         re.compile("https?://(data-)?servicediscovery\\.(.+)\\.amazonaws\\.com"),
diff --git a/moto/backends.py b/moto/backends.py
index deac05171..3fb774948 100644
--- a/moto/backends.py
+++ b/moto/backends.py
@@ -134,6 +134,7 @@ if TYPE_CHECKING:
     from moto.scheduler.models import EventBridgeSchedulerBackend
     from moto.sdb.models import SimpleDBBackend
     from moto.secretsmanager.models import SecretsManagerBackend
+    from moto.servicecatalogappregistry.models import AppRegistryBackend
     from moto.servicediscovery.models import ServiceDiscoveryBackend
     from moto.servicequotas.models import ServiceQuotasBackend
     from moto.ses.models import SESBackend
@@ -322,6 +323,7 @@ SERVICE_NAMES = Union[
     "Literal['scheduler']",
     "Literal['sdb']",
     "Literal['secretsmanager']",
+    "Literal['servicecatalogappregistry']",
     "Literal['servicediscovery']",
     "Literal['service-quotas']",
     "Literal['ses']",
@@ -714,6 +716,10 @@ def get_backend(
     name: "Literal['secretsmanager']",
 ) -> "BackendDict[SecretsManagerBackend]": ...
 @overload
+def get_backend(
+    name: "Literal['servicecatalogappregistry']",
+) -> "BackendDict[AppRegistryBackend]": ...
+@overload
 def get_backend(
     name: "Literal['servicediscovery']",
 ) -> "BackendDict[ServiceDiscoveryBackend]": ...
diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py
index 26eb36438..4ae7eaaf7 100644
--- a/moto/ec2/exceptions.py
+++ b/moto/ec2/exceptions.py
@@ -59,6 +59,16 @@ class DependencyViolationError(EC2ClientError):
         super().__init__("DependencyViolation", message)
 
 
+class MissingInputError(EC2ClientError):
+    def __init__(self, message: str):
+        super().__init__("MissingInput", message)
+
+
+class InvalidInputError(EC2ClientError):
+    def __init__(self, message: str):
+        super().__init__("InvalidInput", message)
+
+
 class MissingParameterError(EC2ClientError):
     def __init__(self, parameter: str):
         super().__init__(
@@ -114,6 +124,14 @@ class InvalidKeyPairFormatError(EC2ClientError):
         )
 
 
+class VPCIdNotSpecifiedError(EC2ClientError):
+    def __init__(self) -> None:
+        super().__init__(
+            "VPCIdNotSpecified",
+            "No default VPC for this user. GroupName is only supported for EC2-Classic and default VPC.",
+        )
+
+
 class InvalidVPCIdError(EC2ClientError):
     def __init__(self, vpc_id: Any):
         super().__init__("InvalidVpcID.NotFound", f"VpcID {vpc_id} does not exist.")
diff --git a/moto/ec2/models/instances.py b/moto/ec2/models/instances.py
index 953e959c6..daf0eb2c7 100644
--- a/moto/ec2/models/instances.py
+++ b/moto/ec2/models/instances.py
@@ -21,14 +21,17 @@ from moto.packages.boto.ec2.instance import Reservation
 
 from ..exceptions import (
     AvailabilityZoneNotFromRegionError,
+    InvalidInputError,
     InvalidInstanceIdError,
     InvalidInstanceTypeError,
     InvalidParameterCombination,
     InvalidParameterValueErrorUnknownAttribute,
     InvalidSecurityGroupNotFoundError,
     InvalidSubnetIdError,
+    MissingInputError,
     OperationDisableApiStopNotPermitted,
     OperationNotPermitted4,
+    VPCIdNotSpecifiedError,
 )
 from ..utils import (
     convert_tag_spec,
@@ -529,7 +532,26 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel):
                 else:
                     # Get default Subnet
                     default_subnets = self.ec2_backend.get_default_subnets()
-                    nic_subnet = default_subnets[self._placement.zone]
+
+                    default_subnet = None
+
+                    if self.subnet_id is None:
+                        default_vpc = self.ec2_backend.get_default_vpc()
+                        if default_vpc is None:
+                            raise VPCIdNotSpecifiedError()
+
+                        if not default_subnets:
+                            raise MissingInputError(
+                                f"No subnets found for the default VPC '{default_vpc.id}'. Please specify a subnet."
+                            )
+
+                        default_subnet = default_subnets.get(self._placement.zone)
+                        if default_subnet is None:
+                            raise InvalidInputError(
+                                f"No default subnet for availability zone: '{self._placement.zone}'."
+                            )
+
+                    nic_subnet = default_subnet or self.subnet_id  # type: ignore[assignment]
 
                 group_ids = nic.get("SecurityGroupId") or []
                 if security_groups:
diff --git a/moto/organizations/models.py b/moto/organizations/models.py
index cb7057efa..a70abe725 100644
--- a/moto/organizations/models.py
+++ b/moto/organizations/models.py
@@ -257,6 +257,8 @@ class FakePolicy(BaseModel):
             self._arn_format = utils.AI_POLICY_ARN_FORMAT
         elif self.type == "SERVICE_CONTROL_POLICY":
             self._arn_format = utils.SCP_ARN_FORMAT
+        elif self.type == "TAG_POLICY":
+            self._arn_format = utils.TAG_POLICY_ARN_FORMAT
         else:
             raise NotImplementedError(
                 f"The {self.type} policy type has not been implemented"
@@ -778,7 +780,11 @@ class OrganizationsBackend(BaseBackend):
         if not FakePolicy.supported_policy_type(_filter):
             raise InvalidInputException("You specified an invalid value.")
 
-        if _filter not in ["AISERVICES_OPT_OUT_POLICY", "SERVICE_CONTROL_POLICY"]:
+        if _filter not in [
+            "AISERVICES_OPT_OUT_POLICY",
+            "SERVICE_CONTROL_POLICY",
+            "TAG_POLICY",
+        ]:
             raise NotImplementedError(
                 f"The {_filter} policy type has not been implemented"
             )
diff --git a/moto/organizations/utils.py b/moto/organizations/utils.py
index 75560c3ec..37368471c 100644
--- a/moto/organizations/utils.py
+++ b/moto/organizations/utils.py
@@ -15,6 +15,7 @@ SCP_ARN_FORMAT = "arn:{0}:organizations::{1}:policy/{2}/service_control_policy/{
 AI_POLICY_ARN_FORMAT = (
     "arn:{0}:organizations::{1}:policy/{2}/aiservices_opt_out_policy/{3}"
 )
+TAG_POLICY_ARN_FORMAT = "arn:{0}:organizations::{1}:policy/{2}/tag_policy/{3}"
 
 CHARSET = string.ascii_lowercase + string.digits
 ORG_ID_SIZE = 10
diff --git a/moto/s3/exceptions.py b/moto/s3/exceptions.py
index bfd600f7f..c077ee829 100644
--- a/moto/s3/exceptions.py
+++ b/moto/s3/exceptions.py
@@ -130,6 +130,16 @@ class MissingVersion(S3ClientError):
         super().__init__("NoSuchVersion", "The specified version does not exist.")
 
 
+class MissingInventoryConfig(S3ClientError):
+    code = 404
+
+    def __init__(self) -> None:
+        super().__init__(
+            "NoSuchInventoryConfig",
+            "The specified inventory configuration does not exist.",
+        )
+
+
 class InvalidVersion(S3ClientError):
     code = 400
 
diff --git a/moto/s3/models.py b/moto/s3/models.py
index 6350801e7..3ea17db67 100644
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -56,6 +56,7 @@ from moto.s3.exceptions import (
     MalformedXML,
     MethodNotAllowed,
     MissingBucket,
+    MissingInventoryConfig,
     MissingKey,
     NoSuchPublicAccessBlockConfiguration,
     NoSuchUpload,
@@ -1067,6 +1068,7 @@ class FakeBucket(CloudFormationModel):
         self.default_lock_days: Optional[int] = 0
         self.default_lock_years: Optional[int] = 0
         self.ownership_rule: Optional[Dict[str, Any]] = None
+        self.inventory_configs: Dict[str, FakeBucketInventoryConfiguration] = {}
         s3_backends.bucket_accounts[name] = (self.partition, account_id)
 
     @property
@@ -1648,6 +1650,24 @@ class FakeBucket(CloudFormationModel):
 class FakeTableStorageBucket(FakeBucket): ...
 
 
+class FakeBucketInventoryConfiguration(BaseModel):
+    def __init__(
+        self,
+        id: str,
+        destination: Dict[str, Any],
+        is_enabled: bool,
+        schedule: Dict[str, Any],
+        filters: Optional[Dict[str, Any]] = None,
+        optional_fields: Optional[List[str]] = None,
+    ):
+        self.id = id
+        self.destination = destination
+        self.is_enabled = is_enabled
+        self.schedule = schedule
+        self.filters = filters
+        self.optional_fields = optional_fields
+
+
 class S3Backend(BaseBackend, CloudWatchMetricProvider):
     """
     Custom S3 endpoints are supported, if you are using a S3-compatible storage solution like Ceph.
@@ -1716,6 +1736,7 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
         self.table_buckets: Dict[str, FakeTableStorageBucket] = {}
         self.tagger = TaggingService()
         self._pagination_tokens: Dict[str, str] = {}
+        self.inventory_configs: Dict[str, FakeBucketInventoryConfiguration] = {}
 
     def reset(self) -> None:
         # For every key and multipart, Moto opens a TemporaryFile to write the value of those keys
@@ -3119,6 +3140,38 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
         # Implementation part of responses.py
         pass
 
+    def put_bucket_inventory_configuration(
+        self, bucket_name: str, inventory_configuration: Dict[str, Any]
+    ) -> None:
+        bucket = self.get_bucket(bucket_name)
+        inv_config = FakeBucketInventoryConfiguration(
+            id=inventory_configuration["Id"],
+            destination=inventory_configuration["Destination"],
+            is_enabled=inventory_configuration["IsEnabled"],
+            schedule=inventory_configuration["Schedule"],
+            filters=inventory_configuration.get("Filter"),
+            optional_fields=inventory_configuration.get("OptionalFields"),
+        )
+
+        bucket.inventory_configs[inv_config.id] = inv_config
+        return
+
+    def get_bucket_inventory_configuration(
+        self, bucket_name: str, id: str, expected_bucket_owner: Optional[str] = None
+    ) -> FakeBucketInventoryConfiguration:
+        bucket = self.get_bucket(bucket_name)
+
+        if id in bucket.inventory_configs:
+            return bucket.inventory_configs[id]
+
+        raise MissingInventoryConfig()
+
+    def list_bucket_inventory_configurations(
+        self, bucket_name: str
+    ) -> List[FakeBucketInventoryConfiguration]:
+        bucket = self.get_bucket(bucket_name)
+        return list(bucket.inventory_configs.values())
+
 
 class S3BackendDict(BackendDict[S3Backend]):
     """
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
index aac4b7d25..48f5220e2 100644
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -353,7 +353,6 @@ class S3Response(BaseResponse):
     ) -> Union[str, TYPE_RESPONSE]:
         querystring = self._get_querystring(request, full_url)
         method = request.method
-
         bucket_name = self.parse_bucket_name_from_url(request, full_url)
         if not bucket_name:
             # If no bucket specified, list all buckets
@@ -555,7 +554,12 @@ class S3Response(BaseResponse):
             return self.get_bucket_accelerate_configuration()
         elif "publicAccessBlock" in querystring:
             return self.get_public_access_block()
-
+        elif "inventory" in querystring:
+            # Only GET includes "id" in the querystring, LIST does not
+            if "id" in querystring:
+                return self.get_bucket_inventory_configuration()
+            else:
+                return self.list_bucket_inventory_configurations()
         elif "versions" in querystring:
             return self.list_object_versions()
         elif "encryption" in querystring:
@@ -886,7 +890,12 @@ class S3Response(BaseResponse):
                 bucket_name, ownership=ownership_rule
             )
             return ""
-
+        elif "inventory" in querystring:
+            inventory_config = self._inventory_config_from_body()
+            self.backend.put_bucket_inventory_configuration(
+                bucket_name, inventory_config
+            )
+            return ""
         else:
             # us-east-1, the default AWS region behaves a bit differently
             # - you should not use any location constraint
@@ -1083,6 +1092,29 @@ class S3Response(BaseResponse):
         template = self.response_template(S3_PUBLIC_ACCESS_BLOCK_CONFIGURATION)
         return template.render(public_block_config=public_block_config)
 
+    def get_bucket_inventory_configuration(self) -> str:
+        config_id = self.querystring["id"][0]
+        inventory_configuration = self.backend.get_bucket_inventory_configuration(
+            bucket_name=self.bucket_name, id=config_id
+        )
+        template = self.response_template(S3_BUCKET_INVENTORY_CONFIGURATION)
+        return template.render(
+            inventory_config=inventory_configuration,
+            s3_bucket_config=inventory_configuration.destination["S3BucketDestination"],
+        )
+
+    def list_bucket_inventory_configurations(self) -> str:
+        inventory_configuration_list = (
+            self.backend.list_bucket_inventory_configurations(
+                bucket_name=self.bucket_name,
+            )
+        )
+        template = self.response_template(LIST_BUCKET_INVENTORY_CONFIGURATIONS_TEMPLATE)
+        # TODO: Add support for pagination/ continuation tokens
+        return template.render(
+            inventory_configuration_list=inventory_configuration_list,
+        )
+
     def _bucket_response_delete(
         self, bucket_name: str, querystring: Dict[str, Any]
     ) -> TYPE_RESPONSE:
@@ -2468,6 +2500,11 @@ class S3Response(BaseResponse):
         config = parsed_xml["ReplicationConfiguration"]
         return config
 
+    def _inventory_config_from_body(self) -> Dict[str, Any]:
+        parsed_xml = xmltodict.parse(self.body)
+        config = parsed_xml["InventoryConfiguration"]
+        return config
+
     def _key_response_delete(
         self, bucket_name: str, query: Dict[str, Any], key_name: str
     ) -> TYPE_RESPONSE:
@@ -3344,6 +3381,97 @@ S3_PUBLIC_ACCESS_BLOCK_CONFIGURATION = """
 </PublicAccessBlockConfiguration>
 """
 
+S3_BUCKET_INVENTORY_CONFIGURATION = """<?xml version="1.0" encoding="UTF-8"?>
+<InventoryConfiguration>
+   <Destination>
+      <S3BucketDestination>
+            {%if s3_bucket_config.get('AccountId') %}
+            <AccountId>{{s3_bucket_config['AccountId']}}</AccountId>
+            {% endif %}
+            <Bucket>{{s3_bucket_config['Bucket']}}</Bucket>
+            <Format>{{s3_bucket_config['Format']}}</Format>
+            {%if s3_bucket_config.get('Prefix') %}
+            <Prefix>{{s3_bucket_config['Prefix']}}</Prefix>
+            {% endif %}
+            {% if s3_bucket_config.get('Encryption') %}
+            <Encryption>
+                ## NOTE boto changes the key SSEKMS to SSE-KMS on put and SSE-KMS to SSEKMS on get
+                {% if s3_bucket_config['Encryption'].get('SSE-KMS') %}
+                <SSE-KMS>
+                    <KeyId>{{s3_bucket_config['Encryption']['SSE-KMS']['KeyId']}}</KeyId>
+                </SSE-KMS>
+                {% else %}
+                <SSES3/>
+                {% endif %}
+            </Encryption>
+            {% endif %}
+      </S3BucketDestination>
+   </Destination>
+   <IsEnabled>{{inventory_config.is_enabled}}</IsEnabled>
+   <Filter>
+      <Prefix>{{inventory_config.filters['Prefix']}}</Prefix>
+   </Filter>
+   <Id>{{inventory_config.id}}</Id>
+   <IncludedObjectVersions>All</IncludedObjectVersions>
+   <OptionalFields>
+        {% for field in inventory_config.optional_fields['Field'] %}
+        <Field>{{ field }}</Field>
+        {% endfor %}
+   </OptionalFields>
+   <Schedule>
+      <Frequency>{{inventory_config.schedule['Frequency']}}</Frequency>
+   </Schedule>
+</InventoryConfiguration>
+"""
+
+LIST_BUCKET_INVENTORY_CONFIGURATIONS_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
+<ListInventoryConfigurationsResult>
+    <IsTruncated>false</IsTruncated>
+    {% for inventory_config in inventory_configuration_list %}
+    <InventoryConfiguration>
+        <Destination>
+            <S3BucketDestination>
+                    {%if inventory_config.destination["S3BucketDestination"].get('AccountId') %}
+                    <AccountId>{{inventory_config.destination["S3BucketDestination"]['AccountId']}}</AccountId>
+                    {% endif %}
+                    <Bucket>{{inventory_config.destination["S3BucketDestination"]['Bucket']}}</Bucket>
+                    <Format>{{inventory_config.destination["S3BucketDestination"]['Format']}}</Format>
+                    {%if inventory_config.destination["S3BucketDestination"].get('Prefix') %}
+                    <Prefix>{{inventory_config.destination["S3BucketDestination"]['Prefix']}}</Prefix>
+                    {% endif %}
+                    {% if inventory_config.destination["S3BucketDestination"].get('Encryption') %}
+                    <Encryption>
+                        ## NOTE boto changes the key SSEKMS to SSE-KMS on put and SSE-KMS to SSEKMS on get
+                        {% if inventory_config.destination["S3BucketDestination"]['Encryption'].get('SSE-KMS') %}
+                        <SSE-KMS>
+                            <KeyId>{{inventory_config.destination["S3BucketDestination"]['Encryption']['SSE-KMS']['KeyId']}}</KeyId>
+                        </SSE-KMS>
+                        {% else %}
+                        <SSES3/>
+                        {% endif %}
+                    </Encryption>
+                    {% endif %}
+            </S3BucketDestination>
+        </Destination>
+        <IsEnabled>{{inventory_config.is_enabled}}</IsEnabled>
+        <Filter>
+            <Prefix>{{inventory_config.filters['Prefix']}}</Prefix>
+        </Filter>
+        <Id>{{inventory_config.id}}</Id>
+        <IncludedObjectVersions>All</IncludedObjectVersions>
+        <OptionalFields>
+                {% for field in inventory_config.optional_fields['Field'] %}
+                <Field>{{ field }}</Field>
+                {% endfor %}
+        </OptionalFields>
+        <Schedule>
+            <Frequency>{{inventory_config.schedule['Frequency']}}</Frequency>
+        </Schedule>
+    </InventoryConfiguration>
+    {% endfor %}
+</ListInventoryConfigurationsResult>
+"""
+
 S3_BUCKET_LOCK_CONFIGURATION = """
 <ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
     {%if lock_enabled %}
diff --git a/moto/servicecatalog/urls.py b/moto/servicecatalog/urls.py
index 785a6308f..a72ae3a41 100644
--- a/moto/servicecatalog/urls.py
+++ b/moto/servicecatalog/urls.py
@@ -1,5 +1,8 @@
 """servicecatalog base URL and path."""
 
+from moto.servicecatalogappregistry.responses import AppRegistryResponse
+from moto.servicecatalogappregistry.urls import url_paths as catalog_paths
+
 from .responses import ServiceCatalogResponse
 
 url_bases = [
@@ -8,4 +11,5 @@ url_bases = [
 
 url_paths = {
     "{0}/$": ServiceCatalogResponse.dispatch,
+    **{path: AppRegistryResponse.dispatch for path in catalog_paths},
 }
diff --git a/moto/servicecatalogappregistry/__init__.py b/moto/servicecatalogappregistry/__init__.py
new file mode 100644
index 000000000..7d72cb3d6
--- /dev/null
+++ b/moto/servicecatalogappregistry/__init__.py
@@ -0,0 +1,1 @@
+from .models import servicecatalogappregistry_backends  # noqa: F401
diff --git a/moto/servicecatalogappregistry/exceptions.py b/moto/servicecatalogappregistry/exceptions.py
new file mode 100644
index 000000000..dff77b7af
--- /dev/null
+++ b/moto/servicecatalogappregistry/exceptions.py
@@ -0,0 +1,15 @@
+"""Exceptions raised by the servicecatalogappregistry service."""
+
+from moto.core.exceptions import JsonRESTError
+
+
+class ResourceNotFoundException(JsonRESTError):
+    def __init__(self, msg: str = "The specified resource does not exist.") -> None:
+        super().__init__("ResourceNotFoundException", msg)
+
+
+class ValidationException(JsonRESTError):
+    def __init__(self) -> None:
+        super().__init__(
+            "ValidationException", "The request has invalid or missing parameters."
+        )
diff --git a/moto/servicecatalogappregistry/models.py b/moto/servicecatalogappregistry/models.py
new file mode 100644
index 000000000..830d2d223
--- /dev/null
+++ b/moto/servicecatalogappregistry/models.py
@@ -0,0 +1,163 @@
+"""AppRegistryBackend class with methods for supported APIs."""
+
+import datetime
+import re
+from typing import Any, Dict, List
+
+from moto.cloudformation.exceptions import ValidationError
+from moto.cloudformation.models import cloudformation_backends
+from moto.core.base_backend import BackendDict, BaseBackend
+from moto.core.common_models import BaseModel
+from moto.moto_api._internal import mock_random
+from moto.resourcegroups.models import FakeResourceGroup
+from moto.servicecatalogappregistry.exceptions import (
+    ResourceNotFoundException,
+    ValidationException,
+)
+from moto.utilities.tagging_service import TaggingService
+from moto.utilities.utils import get_partition
+
+
+class Application(BaseModel):
+    def __init__(
+        self,
+        name: str,
+        description: str,
+        region: str,
+        account_id: str,
+    ):
+        self.id = mock_random.get_random_string(
+            length=27, include_digits=True, lower_case=True
+        )
+        self.arn = f"arn:{get_partition(region)}:servicecatalog:{region}:{account_id}:applications/{self.id}"
+        self.name = name
+        self.description = description
+        self.creationTime = datetime.datetime.now()
+        self.lastUpdateTime = self.creationTime
+        self.tags: Dict[str, str] = dict()
+        self.applicationTag: Dict[str, str] = {"awsApplication": self.arn}
+
+        self.associated_resources: Dict[str, AssociatedResource] = dict()
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "id": self.id,
+            "arn": self.arn,
+            "name": self.name,
+            "description": self.description,
+            "creationTime": str(self.creationTime),
+            "lastUpdateTime": str(self.lastUpdateTime),
+            "tags": self.tags,
+            "applicationTag": self.applicationTag,
+        }
+
+
+class AssociatedResource(BaseBackend):
+    def __init__(
+        self,
+        resource_type: str,
+        resource: str,
+        options: List[str],
+        application: Application,
+        account_id: str,
+        region_name: str,
+    ):
+        if resource_type == "CFN_STACK":
+            self.resource = resource
+            match = re.search(
+                r"^arn:aws:cloudformation:(us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-\d:\d{12}:stack/\w[a-zA-Z0-9\-]{0,127}/[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$",
+                resource,
+            )
+            if match is not None:
+                self.name = resource.split("/")[1]
+            else:
+                self.name = resource
+            cf_backend = cloudformation_backends[account_id][region_name]
+            try:
+                cf_backend.get_stack(self.name)
+            except ValidationError:
+                raise ResourceNotFoundException(
+                    f"No CloudFormation stack called '{self.name}' found"
+                )
+        elif resource_type == "RESOURCE_TAG_VALUE":
+            tags = {
+                "EnableAWSServiceCatalogAppRegistry": "true",
+                "aws:servicecatalog:applicationName": application.name,
+                "aws:servicecatalog:applicationId": application.id,
+                "aws:servicecatalog:applicationArn": application.arn,
+            }
+            new_resource_group = FakeResourceGroup(
+                account_id,
+                region_name,
+                f"AWS_AppRegistry_AppTag_{account_id}-{application.name}",
+                {"Type": "TAG_FILTERS_1_0", "Query": resource},
+                None,
+                tags,
+            )
+            self.resource = new_resource_group.arn
+            self.name = new_resource_group._name
+            self.query = resource
+        else:
+            raise ValidationException
+        self.resource_type = resource_type
+        self.options = options
+
+    def to_json(self) -> Dict[str, Any]:
+        return_dict = {
+            "name": self.name,
+            "arn": self.resource,
+            "resourceType": self.resource_type,
+            "options": self.options,
+        }
+        if self.resource_type == "RESOURCE_TAG_VALUE":
+            return_dict["resourceDetails"] = {"tagValue": self.query}  # type: ignore
+        return return_dict
+
+
+class AppRegistryBackend(BaseBackend):
+    """Implementation of AppRegistry APIs."""
+
+    def __init__(self, region_name: str, account_id: str):
+        super().__init__(region_name, account_id)
+        self.applications: Dict[str, Application] = dict()
+        self.tagger = TaggingService()
+
+    def create_application(
+        self, name: str, description: str, tags: Dict[str, str], client_token: str
+    ) -> Application:
+        app = Application(
+            name,
+            description,
+            region=self.region_name,
+            account_id=self.account_id,
+        )
+        self.applications[app.arn] = app
+        self._tag_resource(app.arn, tags)
+        return app
+
+    def list_applications(self) -> List[Application]:
+        return list(self.applications.values())
+
+    def associate_resource(
+        self, application: str, resource_type: str, resource: str, options: List[str]
+    ) -> Dict[str, Any]:
+        app = self.applications[application]
+        new_resource = AssociatedResource(
+            resource_type, resource, options, app, self.account_id, self.region_name
+        )
+        app.associated_resources[new_resource.resource] = new_resource
+        return {"applicationArn": app.arn, "resourceArn": resource, "options": options}
+
+    def _list_tags_for_resource(self, arn: str) -> Dict[str, str]:
+        return self.tagger.get_tag_dict_for_resource(arn)
+
+    def _tag_resource(self, arn: str, tags: Dict[str, str]) -> None:
+        self.tagger.tag_resource(arn, TaggingService.convert_dict_to_tags_input(tags))
+
+    def _untag_resource(self, arn: str, tag_keys: List[str]) -> None:
+        self.tagger.untag_resource_using_names(arn, tag_keys)
+
+
+servicecatalogappregistry_backends = BackendDict(
+    AppRegistryBackend, "servicecatalog-appregistry"
+)
diff --git a/moto/servicecatalogappregistry/responses.py b/moto/servicecatalogappregistry/responses.py
new file mode 100644
index 000000000..210238acc
--- /dev/null
+++ b/moto/servicecatalogappregistry/responses.py
@@ -0,0 +1,84 @@
+"""Handles incoming servicecatalogappregistry requests, invokes methods, returns responses."""
+
+import json
+from urllib.parse import unquote
+
+from moto.core.responses import BaseResponse
+from moto.servicecatalogappregistry.exceptions import ValidationException
+
+from .models import (
+    Application,
+    AppRegistryBackend,
+    servicecatalogappregistry_backends,
+)
+
+
+class AppRegistryResponse(BaseResponse):
+    """Handler for AppRegistry requests and responses."""
+
+    def __init__(self) -> None:
+        super().__init__(service_name="servicecatalog-appregistry")
+
+    @property
+    def servicecatalogappregistry_backend(self) -> AppRegistryBackend:
+        """Return backend instance specific for this region."""
+        return servicecatalogappregistry_backends[self.current_account][self.region]
+
+    def create_application(self) -> str:
+        name = self._get_param("name")
+        description = self._get_param("description")
+        tags = self._get_param("tags")
+        client_token = self._get_param("clientToken")
+        application = self.servicecatalogappregistry_backend.create_application(
+            name=name,
+            description=description,
+            tags=tags,
+            client_token=client_token,
+        )
+        return json.dumps({"application": application.to_json()})
+
+    def list_applications(self) -> str:
+        applications = self.servicecatalogappregistry_backend.list_applications()
+        json_list = []
+        for app in applications:
+            json_list.append(app.to_json())
+        return json.dumps({"applications": json_list})
+
+    def associate_resource(self) -> str:
+        application = unquote(self._get_param("application"))
+        resource_type = self._get_param("resourceType")
+        resource = unquote(self._get_param("resource"))
+        options = self._get_param("options")
+        app = None
+        app = self._find_app_by_any_value(application)
+        if options is None:
+            options = []
+        new_resource = self.servicecatalogappregistry_backend.associate_resource(
+            app.arn,
+            resource_type,
+            resource,
+            options,
+        )
+        return json.dumps(new_resource)
+
+    def list_associated_resources(self) -> str:
+        application = unquote(self._get_param("application"))
+        app = self._find_app_by_any_value(application)
+        return_list = []
+        for resource in app.associated_resources.values():
+            return_list.append(resource.to_json())
+        return json.dumps({"resources": return_list})
+
+    def _find_app_by_any_value(self, search: str) -> Application:
+        app = None
+        if search in self.servicecatalogappregistry_backend.applications:
+            app = self.servicecatalogappregistry_backend.applications[search]
+        else:
+            for a in self.servicecatalogappregistry_backend.applications.values():
+                if search == a.id:
+                    app = a
+                elif search == a.name:
+                    app = a
+        if app is None:
+            raise ValidationException
+        return app
diff --git a/moto/servicecatalogappregistry/urls.py b/moto/servicecatalogappregistry/urls.py
new file mode 100644
index 000000000..ab52286c6
--- /dev/null
+++ b/moto/servicecatalogappregistry/urls.py
@@ -0,0 +1,13 @@
+"""servicecatalogappregistry base URL and path."""
+
+from .responses import AppRegistryResponse
+
+url_bases = [
+    r"https?://servicecatalog-appregistry\.(.+)\.amazonaws\.com",
+]
+
+url_paths = {
+    "{0}/applications$": AppRegistryResponse.dispatch,
+    "{0}/applications/(?P<application>[^/]+)/resources/(?P<resource_type>[^/]+)/(?P<resource>.+)$": AppRegistryResponse.dispatch,
+    "{0}/applications/(?P<application>.+)/resources$": AppRegistryResponse.dispatch,
+}
diff --git a/moto/timestreaminfluxdb/models.py b/moto/timestreaminfluxdb/models.py
index ce92e2a29..ce819b151 100644
--- a/moto/timestreaminfluxdb/models.py
+++ b/moto/timestreaminfluxdb/models.py
@@ -1,10 +1,11 @@
 """TimestreamInfluxDBBackend class with methods for supported APIs."""
 
 from enum import Enum
-from typing import Any, Dict, List, Optional
+from typing import Any, Dict, List, Optional, Tuple
 
 from moto.core.base_backend import BackendDict, BaseBackend
 from moto.core.common_models import BaseModel
+from moto.utilities.paginator import paginate
 from moto.utilities.tagging_service import TaggingService
 
 from .exceptions import (
@@ -14,6 +15,21 @@ from .exceptions import (
 )
 from .utils import random_id, validate_name
 
+PAGINATION_MODEL = {
+    "list_db_parameter_groups": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,
+        "unique_attribute": "id",
+    },
+    "list_db_clusters": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,
+        "unique_attribute": "id",
+    },
+}
+
 
 class InstanceStatus(str, Enum):
     CREATING = "CREATING"
@@ -54,6 +70,131 @@ class DeploymentType(str, Enum):
     WITH_MULTIAZ_STANDBY = "WITH_MULTIAZ_STANDBY"
 
 
+class ParameterGroup(BaseModel):
+    def __init__(
+        self,
+        name: str,
+        description: Optional[str] = None,
+        parameters: Optional[Dict[str, Any]] = None,
+        region_name: str = "",
+        account_id: str = "",
+    ):
+        self.id = random_id()
+        self.name = name
+        self.description = description or ""
+        self.parameters = parameters or {}
+        self.arn = f"arn:aws:timestream-influxdb:{region_name}:{account_id}:db-parameter-group/{self.id}"
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "id": self.id,
+            "name": self.name,
+            "arn": self.arn,
+            "description": self.description,
+            "parameters": self.parameters,
+        }
+
+    def to_summary_dict(self) -> Dict[str, str]:
+        return {
+            "id": self.id,
+            "name": self.name,
+            "arn": self.arn,
+            "description": self.description,
+        }
+
+
+class Cluster(BaseModel):
+    def __init__(
+        self,
+        name: str,
+        password: str,
+        username: Optional[str] = None,
+        organization: Optional[str] = None,
+        bucket: Optional[str] = None,
+        port: Optional[int] = None,
+        db_parameter_group_identifier: Optional[str] = None,
+        db_instance_type: Optional[str] = None,
+        db_storage_type: Optional[str] = None,
+        allocated_storage: Optional[int] = None,
+        network_type: Optional[str] = None,
+        publicly_accessible: Optional[bool] = None,
+        vpc_subnet_ids: Optional[List[str]] = None,
+        vpc_security_group_ids: Optional[List[str]] = None,
+        deployment_type: Optional[str] = None,
+        failover_mode: Optional[str] = None,
+        log_delivery_configuration: Optional[Dict[str, Any]] = None,
+        region_name: str = "",
+        account_id: str = "",
+        endpoint_id: str = "",
+    ):
+        self.id = random_id()
+        self.name = name
+        self.password = password
+        self.username = username
+        self.organization = organization
+        self.bucket = bucket
+        self.port = port or 8086
+        self.db_parameter_group_identifier = db_parameter_group_identifier
+        self.db_instance_type = db_instance_type or "db.influx.medium"
+        self.db_storage_type = db_storage_type or DBStorageType.InfluxIOIncludedT1
+        self.allocated_storage = allocated_storage or 100
+        self.network_type = network_type or NetworkType.IPV4
+        self.publicly_accessible = publicly_accessible or False
+        self.vpc_subnet_ids = vpc_subnet_ids or ["subnet-default"]
+        self.vpc_security_group_ids = vpc_security_group_ids or ["sg-default"]
+        self.deployment_type = deployment_type or "MULTI_NODE_READ_REPLICAS"
+        self.failover_mode = failover_mode or "AUTOMATIC"
+        self.log_delivery_configuration = log_delivery_configuration or {}
+        self.arn = f"arn:aws:timestream-influxdb:{region_name}:{account_id}:db-cluster/{self.id}"
+        self.endpoint = (
+            f"{self.id}-{endpoint_id}.timestream-influxdb.{region_name}.on.aws"
+        )
+        self.reader_endpoint = (
+            f"{self.id}-{endpoint_id}.reader.timestream-influxdb.{region_name}.on.aws"
+        )
+        self.influx_auth_parameters_secret_arn = f"arn:aws:secretsmanager:{region_name}:{account_id}:secret:timestream-influxdb/{self.id}/auth-params-{random_id(6)}"
+        self.status = "CREATING"
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "id": self.id,
+            "name": self.name,
+            "arn": self.arn,
+            "status": self.status,
+            "endpoint": self.endpoint,
+            "readerEndpoint": self.reader_endpoint,
+            "port": self.port,
+            "deploymentType": self.deployment_type,
+            "dbInstanceType": self.db_instance_type,
+            "networkType": self.network_type,
+            "dbStorageType": self.db_storage_type,
+            "allocatedStorage": self.allocated_storage,
+            "publiclyAccessible": self.publicly_accessible,
+            "dbParameterGroupIdentifier": self.db_parameter_group_identifier,
+            "logDeliveryConfiguration": self.log_delivery_configuration,
+            "influxAuthParametersSecretArn": self.influx_auth_parameters_secret_arn,
+            "vpcSubnetIds": self.vpc_subnet_ids,
+            "vpcSecurityGroupIds": self.vpc_security_group_ids,
+            "failoverMode": self.failover_mode,
+        }
+
+    def to_summary_dict(self) -> Dict[str, Any]:
+        return {
+            "id": self.id,
+            "name": self.name,
+            "arn": self.arn,
+            "status": self.status,
+            "endpoint": self.endpoint,
+            "readerEndpoint": self.reader_endpoint,
+            "port": self.port,
+            "deploymentType": self.deployment_type,
+            "dbInstanceType": self.db_instance_type,
+            "networkType": self.network_type,
+            "dbStorageType": self.db_storage_type,
+            "allocatedStorage": self.allocated_storage,
+        }
+
+
 class DBInstance(BaseModel):
     def __init__(
         self,
@@ -144,6 +285,8 @@ class TimestreamInfluxDBBackend(BaseBackend):
         # https://docs.aws.amazon.com/timestream/latest/developerguide/timestream-for-influxdb.html
         self.endpoint_id: str = random_id(10)
         self.db_instances: Dict[str, DBInstance] = {}
+        self.db_parameter_groups: Dict[str, ParameterGroup] = {}
+        self.db_clusters: Dict[str, Cluster] = {}
         self.tagger = TaggingService()
 
     def create_db_instance(
@@ -270,6 +413,139 @@ class TimestreamInfluxDBBackend(BaseBackend):
     def list_tags_for_resource(self, resource_arn: str) -> Dict[str, str]:
         return self.tagger.get_tag_dict_for_resource(resource_arn)
 
+    def create_db_parameter_group(
+        self,
+        name: str,
+        description: Optional[str] = None,
+        parameters: Optional[Dict[str, Any]] = None,
+        tags: Optional[Dict[str, str]] = None,
+    ) -> ParameterGroup:
+        validate_name(name)
+
+        for param_group in self.db_parameter_groups.values():
+            if param_group.name == name:
+                raise ConflictException(
+                    f"A DB parameter group with the name {name} already exists"
+                )
+
+        param_group = ParameterGroup(
+            name=name,
+            description=description,
+            parameters=parameters,
+            region_name=self.region_name,
+            account_id=self.account_id,
+        )
+
+        self.db_parameter_groups[param_group.id] = param_group
+
+        if tags:
+            self.tag_resource(param_group.arn, tags)
+
+        return param_group
+
+    def get_db_parameter_group(self, identifier: str) -> ParameterGroup:
+        param_group = self.db_parameter_groups.get(identifier)
+        if not param_group:
+            raise ResourceNotFoundException(
+                f"DB parameter group with identifier {identifier} not found"
+            )
+
+        return param_group
+
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_db_parameter_groups(self) -> List[Dict[str, str]]:
+        if not self.db_parameter_groups:
+            return []
+
+        return [
+            param_group.to_summary_dict()
+            for param_group in self.db_parameter_groups.values()
+        ]
+
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_db_clusters(self) -> List[Dict[str, object]]:
+        if not self.db_clusters:
+            return []
+
+        return [
+            cluster.to_summary_dict()
+            for cluster in self.db_clusters.values()
+            if cluster.status != "DELETED"
+        ]
+
+    def get_db_cluster(self, db_cluster_id: str) -> Cluster:
+        cluster = self.db_clusters.get(db_cluster_id)
+        if not cluster:
+            raise ResourceNotFoundException(
+                f"DB cluster with ID {db_cluster_id} not found"
+            )
+
+        return cluster
+
+    def create_db_cluster(
+        self,
+        name: str,
+        password: str,
+        username: Optional[str] = None,
+        organization: Optional[str] = None,
+        bucket: Optional[str] = None,
+        port: Optional[int] = None,
+        db_parameter_group_identifier: Optional[str] = None,
+        db_instance_type: Optional[str] = None,
+        db_storage_type: Optional[str] = None,
+        allocated_storage: Optional[int] = None,
+        network_type: Optional[str] = None,
+        publicly_accessible: Optional[bool] = None,
+        vpc_subnet_ids: Optional[List[str]] = None,
+        vpc_security_group_ids: Optional[List[str]] = None,
+        deployment_type: Optional[str] = None,
+        failover_mode: Optional[str] = None,
+        log_delivery_configuration: Optional[Dict[str, Any]] = None,
+        tags: Optional[Dict[str, str]] = None,
+    ) -> Tuple[str, str]:
+        validate_name(name)
+
+        for cluster in self.db_clusters.values():
+            if cluster.name == name:
+                raise ConflictException(
+                    f"A DB cluster with the name {name} already exists"
+                )
+
+        new_cluster = Cluster(
+            name=name,
+            password=password,
+            username=username,
+            organization=organization,
+            bucket=bucket,
+            port=port,
+            db_parameter_group_identifier=db_parameter_group_identifier,
+            db_instance_type=db_instance_type,
+            db_storage_type=db_storage_type,
+            allocated_storage=allocated_storage,
+            network_type=network_type,
+            publicly_accessible=publicly_accessible,
+            vpc_subnet_ids=vpc_subnet_ids,
+            vpc_security_group_ids=vpc_security_group_ids,
+            deployment_type=deployment_type,
+            failover_mode=failover_mode,
+            log_delivery_configuration=log_delivery_configuration,
+            region_name=self.region_name,
+            account_id=self.account_id,
+            endpoint_id=self.endpoint_id,
+        )
+
+        new_cluster.status = "AVAILABLE"
+
+        self.db_clusters[new_cluster.id] = new_cluster
+
+        if tags:
+            self.tag_resource(new_cluster.arn, tags)
+
+        return (
+            new_cluster.id,
+            "AVAILABLE",
+        )
+
 
 timestreaminfluxdb_backends = BackendDict(
     TimestreamInfluxDBBackend,
diff --git a/moto/timestreaminfluxdb/responses.py b/moto/timestreaminfluxdb/responses.py
index b6b9dece9..a8c7b7a2f 100644
--- a/moto/timestreaminfluxdb/responses.py
+++ b/moto/timestreaminfluxdb/responses.py
@@ -103,3 +103,119 @@ class TimestreamInfluxDBResponse(BaseResponse):
         arn = params.get("resourceArn")
         tags = self.timestreaminfluxdb_backend.list_tags_for_resource(resource_arn=arn)
         return json.dumps({"tags": tags})
+
+    def create_db_parameter_group(self) -> str:
+        params = json.loads(self.body)
+        name = params.get("name")
+        description = params.get("description")
+        parameters = params.get("parameters")
+        tags = params.get("tags")
+
+        param_group = self.timestreaminfluxdb_backend.create_db_parameter_group(
+            name=name,
+            description=description,
+            parameters=parameters,
+            tags=tags,
+        )
+
+        return json.dumps(param_group.to_dict())
+
+    def get_db_parameter_group(self) -> str:
+        params = json.loads(self.body)
+        identifier = params.get("identifier")
+
+        param_group = self.timestreaminfluxdb_backend.get_db_parameter_group(
+            identifier=identifier,
+        )
+
+        return json.dumps(param_group.to_dict())
+
+    def list_db_parameter_groups(self) -> str:
+        params = json.loads(self.body)
+        next_token = params.get("nextToken")
+        max_results = params.get("maxResults")
+
+        items, next_token = self.timestreaminfluxdb_backend.list_db_parameter_groups(
+            next_token=next_token,
+            max_results=max_results,
+        )
+
+        response = {"items": items}
+        if next_token:
+            response["nextToken"] = next_token
+
+        return json.dumps(response)
+
+    def list_db_clusters(self) -> str:
+        params = json.loads(self.body)
+        next_token = params.get("nextToken")
+        max_results = params.get("maxResults")
+
+        items, next_token = self.timestreaminfluxdb_backend.list_db_clusters(
+            next_token=next_token,
+            max_results=max_results,
+        )
+
+        response = {"items": items}
+        if next_token:
+            response["nextToken"] = next_token
+
+        return json.dumps(response)
+
+    def get_db_cluster(self) -> str:
+        params = json.loads(self.body)
+        db_cluster_id = params.get("dbClusterId")
+
+        cluster = self.timestreaminfluxdb_backend.get_db_cluster(
+            db_cluster_id=db_cluster_id,
+        )
+
+        return json.dumps(cluster.to_dict())
+
+    def create_db_cluster(self) -> str:
+        params = json.loads(self.body)
+        name = params.get("name")
+        username = params.get("username")
+        password = params.get("password")
+        organization = params.get("organization")
+        bucket = params.get("bucket")
+        port = params.get("port")
+        db_parameter_group_identifier = params.get("dbParameterGroupIdentifier")
+        db_instance_type = params.get("dbInstanceType")
+        db_storage_type = params.get("dbStorageType")
+        allocated_storage = params.get("allocatedStorage")
+        network_type = params.get("networkType")
+        publicly_accessible = params.get("publiclyAccessible")
+        vpc_subnet_ids = params.get("vpcSubnetIds")
+        vpc_security_group_ids = params.get("vpcSecurityGroupIds")
+        deployment_type = params.get("deploymentType")
+        failover_mode = params.get("failoverMode")
+        log_delivery_configuration = params.get("logDeliveryConfiguration")
+        tags = params.get("tags")
+
+        db_cluster_id, db_cluster_status = (
+            self.timestreaminfluxdb_backend.create_db_cluster(
+                name=name,
+                username=username,
+                password=password,
+                organization=organization,
+                bucket=bucket,
+                port=port,
+                db_parameter_group_identifier=db_parameter_group_identifier,
+                db_instance_type=db_instance_type,
+                db_storage_type=db_storage_type,
+                allocated_storage=allocated_storage,
+                network_type=network_type,
+                publicly_accessible=publicly_accessible,
+                vpc_subnet_ids=vpc_subnet_ids,
+                vpc_security_group_ids=vpc_security_group_ids,
+                deployment_type=deployment_type,
+                failover_mode=failover_mode,
+                log_delivery_configuration=log_delivery_configuration,
+                tags=tags,
+            )
+        )
+
+        return json.dumps(
+            {"dbClusterId": db_cluster_id, "dbClusterStatus": db_cluster_status}
+        )
diff --git a/scripts/scaffold.py b/scripts/scaffold.py
index e53354f9c..4b98244c7 100755
--- a/scripts/scaffold.py
+++ b/scripts/scaffold.py
@@ -448,6 +448,10 @@ def get_response_restxml_template(service, operation):
         list(client._service_model._service_description["operations"].keys()),
     )
     op_model = client._service_model.operation_model(aws_operation_name)
+    # Resolves https://github.com/getmoto/moto/issues/8726
+    if not hasattr(op_model.output_shape, "output"):
+        # Response has no output
+        return ""
     result_wrapper = op_model._operation_model["output"]["shape"]
     response_wrapper = result_wrapper.replace("Result", "Response")
     metadata = op_model.metadata


Test patch:
diff --git a/.github/workflows/tests_sdk_ruby.yml b/.github/workflows/tests_sdk_ruby.yml
index 7fcf98d8f..b4c1dc743 100644
--- a/.github/workflows/tests_sdk_ruby.yml
+++ b/.github/workflows/tests_sdk_ruby.yml
@@ -13,7 +13,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - name: Set up Ruby ${{ matrix.ruby-version }}
-        uses: ruby/setup-ruby@e5ac7b085f6e63d49c8973eb0c6e04d876b881f1
+        uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535
         with:
           ruby-version: ${{ matrix.ruby-version }}
       - name: Set up Python 3.12
diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py
index 006f9a2f6..839015562 100644
--- a/tests/test_ec2/test_instances.py
+++ b/tests/test_ec2/test_instances.py
@@ -2836,3 +2836,75 @@ def test_instance_with_ipv6_address():
 
     assert len(instance["NetworkInterfaces"]) == 1
     assert len(instance["NetworkInterfaces"][0]["Ipv6Addresses"]) == 1
+
+
+@mock_aws
+def test_instance_without_default_subnet():
+    # Ensure that in an account without a default subnet, launching instances without specifying a subnet raises.
+    client = boto3.client("ec2", region_name="ap-south-1")
+
+    # Delete all VPCs and subnets
+    subnets = client.describe_subnets()
+    for subnet in subnets["Subnets"]:
+        client.delete_subnet(SubnetId=subnet["SubnetId"])
+    vpcs = client.describe_vpcs()
+    for vpc in vpcs["Vpcs"]:
+        client.delete_vpc(VpcId=vpc["VpcId"])
+
+    # RunInstances must raise
+    with pytest.raises(ClientError) as exc:
+        client.run_instances(ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1)
+    assert exc.value.response["Error"]["Code"] == "VPCIdNotSpecified"
+    assert (
+        exc.value.response["Error"]["Message"]
+        == "No default VPC for this user. GroupName is only supported for EC2-Classic and default VPC."
+    )
+
+    # Create default VPC but without default subnets
+    default_vpc_id = client.create_default_vpc()["Vpc"]["VpcId"]
+    subnets = client.describe_subnets()
+    for subnet in subnets["Subnets"]:
+        client.delete_subnet(SubnetId=subnet["SubnetId"])
+
+    # Ensure RunInstances raises
+    with pytest.raises(ClientError) as exc:
+        client.run_instances(ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1)
+    assert exc.value.response["Error"]["Code"] == "MissingInput"
+    assert (
+        exc.value.response["Error"]["Message"]
+        == f"No subnets found for the default VPC '{default_vpc_id}'. Please specify a subnet."
+    )
+
+    # Create default subnet in an AZ
+    client.create_default_subnet(AvailabilityZone="ap-south-1c")
+
+    # Ensure RunInstances without a subnet, in a different AZ raises
+    with pytest.raises(ClientError) as exc:
+        client.run_instances(
+            ImageId=EXAMPLE_AMI_ID,
+            MinCount=1,
+            MaxCount=1,
+            Placement={"AvailabilityZone": "ap-south-1b"},
+        )
+    assert exc.value.response["Error"]["Code"] == "InvalidInput"
+    assert (
+        exc.value.response["Error"]["Message"]
+        == "No default subnet for availability zone: 'ap-south-1b'."
+    )
+
+    # Ensure RunInstances without a subnet where a default subnet exists succeeds
+    client.run_instances(
+        ImageId=EXAMPLE_AMI_ID,
+        MinCount=1,
+        MaxCount=1,
+        Placement={"AvailabilityZone": "ap-south-1c"},
+    )
+
+    # Create another VPC and subnet
+    vpc = client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]
+    subnet = client.create_subnet(VpcId=vpc["VpcId"], CidrBlock="10.0.1.0/24")["Subnet"]
+
+    # Ensure RunInstances with a subnet specified succeeds
+    client.run_instances(
+        ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1, SubnetId=subnet["SubnetId"]
+    )
diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py
index a860fe722..3170e01ea 100644
--- a/tests/test_organizations/test_organizations_boto3.py
+++ b/tests/test_organizations/test_organizations_boto3.py
@@ -2369,3 +2369,52 @@ def test_aiservices_opt_out_policy():
     )
     assert len(response["Policies"]) == 1
     assert response["Policies"][0]["Id"] == policy_id
+
+
+@mock_aws
+def test_tag_policy():
+    # given
+    client = boto3.client("organizations", region_name="us-east-1")
+    org = client.create_organization(FeatureSet="ALL")["Organization"]
+    root_id = client.list_roots()["Roots"][0]["Id"]
+    client.enable_policy_type(RootId=root_id, PolicyType="TAG_POLICY")
+    tag_policy = {
+        "tags": {
+            "CostCenter": {
+                "tag_key": {"@@assign": "CostCenter"},
+                "tag_value": {"@@assign": ["100", "200", "300"]},
+                "enforced_for": {"@@assign": ["ec2:instance", "s3:bucket"]},
+            }
+        }
+    }
+
+    # when
+    response = client.create_policy(
+        Content=json.dumps(tag_policy),
+        Description="Test tag policy",
+        Name="test-tag-policy",
+        Type="TAG_POLICY",
+    )
+
+    # then
+    summary = response["Policy"]["PolicySummary"]
+    policy_id = summary["Id"]
+    assert re.match(utils.POLICY_ID_REGEX, summary["Id"])
+    assert summary["Arn"] == (
+        utils.TAG_POLICY_ARN_FORMAT.format(
+            "aws", org["MasterAccountId"], org["Id"], summary["Id"]
+        )
+    )
+    assert summary["Name"] == "test-tag-policy"
+    assert summary["Description"] == "Test tag policy"
+    assert summary["Type"] == "TAG_POLICY"
+    assert summary["AwsManaged"] is False
+    assert json.loads(response["Policy"]["Content"]) == tag_policy
+
+    # when
+    client.attach_policy(PolicyId=policy_id, TargetId=root_id)
+
+    # then
+    response = client.list_policies_for_target(TargetId=root_id, Filter="TAG_POLICY")
+    assert len(response["Policies"]) == 1
+    assert response["Policies"][0]["Id"] == policy_id
diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py
index 4815aa317..bd086a306 100644
--- a/tests/test_s3/test_s3.py
+++ b/tests/test_s3/test_s3.py
@@ -3561,3 +3561,132 @@ def enable_versioning(bucket_name, s3_client):
     while resp.get("Status") != "Enabled":
         sleep(0.1)
         resp = s3_client.get_bucket_versioning(Bucket=bucket_name)
+
+
+@mock_aws
+def test_put_and_get_bucket_inventory_configuration():
+    client = boto3.client("s3", region_name="us-east-1")
+    bucket = "mybucket"
+    bucket2 = "mybucket2"
+    id = "my-inventory-config"
+    inventory_configuration = {
+        "Destination": {
+            "S3BucketDestination": {
+                "AccountId": DEFAULT_ACCOUNT_ID,
+                "Bucket": f"arn:aws:s3:::{bucket2}",
+                "Format": "CSV",
+                "Prefix": "test",
+                "Encryption": {"SSEKMS": {"KeyId": "key-12345"}},
+            }
+        },
+        "IsEnabled": True,
+        "Filter": {"Prefix": "folder1/"},
+        "Id": id,
+        "IncludedObjectVersions": "All",
+        "OptionalFields": ["Size", "ETag"],
+        "Schedule": {"Frequency": "Daily"},
+    }
+
+    client.create_bucket(Bucket=bucket)
+    client.create_bucket(Bucket=bucket2)
+    resp = client.put_bucket_inventory_configuration(
+        Bucket=bucket,
+        Id=id,
+        InventoryConfiguration=inventory_configuration,
+        ExpectedBucketOwner=DEFAULT_ACCOUNT_ID,
+    )
+    assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # Retrieve the configuration
+    resp = client.get_bucket_inventory_configuration(Bucket=bucket, Id=id)
+    assert resp["InventoryConfiguration"]["Id"] == id
+    s3_bucket_dest = resp["InventoryConfiguration"]["Destination"][
+        "S3BucketDestination"
+    ]
+    assert s3_bucket_dest["AccountId"] == DEFAULT_ACCOUNT_ID
+    assert s3_bucket_dest["Bucket"] == f"arn:aws:s3:::{bucket2}"
+    assert s3_bucket_dest["Format"] == "CSV"
+    assert s3_bucket_dest["Prefix"] == "test"
+    assert s3_bucket_dest["Encryption"]["SSEKMS"]["KeyId"] == "key-12345"
+    assert resp["InventoryConfiguration"]["IsEnabled"] is True
+    assert resp["InventoryConfiguration"]["Filter"]["Prefix"] == "folder1/"
+    assert resp["InventoryConfiguration"]["IncludedObjectVersions"] == "All"
+    assert resp["InventoryConfiguration"]["OptionalFields"] == ["Size", "ETag"]
+    assert resp["InventoryConfiguration"]["Schedule"]["Frequency"] == "Daily"
+
+
+@mock_aws
+def test_list_bucket_inventory_configurations():
+    client = boto3.client("s3", region_name="us-east-1")
+    bucket = "mybucket"
+    bucket2 = "mybucket2"
+    id = "my-inventory-config"
+
+    client.create_bucket(Bucket=bucket)
+    client.create_bucket(Bucket=bucket2)
+
+    # Create three inventory configurations for the first
+    for i in range(3):
+        inventory_configuration = {
+            "Destination": {
+                "S3BucketDestination": {
+                    "AccountId": DEFAULT_ACCOUNT_ID,
+                    "Bucket": f"arn:aws:s3:::{bucket2}",
+                    "Format": "CSV",
+                    "Prefix": "test",
+                    "Encryption": {"SSEKMS": {"KeyId": "key-12345"}},
+                }
+            },
+            "IsEnabled": True,
+            "Filter": {"Prefix": "folder1/"},
+            "Id": f"{id}-{bucket}-{i}",
+            "IncludedObjectVersions": "All",
+            "OptionalFields": ["Size", "ETag"],
+            "Schedule": {"Frequency": "Daily"},
+        }
+        client.put_bucket_inventory_configuration(
+            Bucket=bucket,
+            Id=id,
+            InventoryConfiguration=inventory_configuration,
+            ExpectedBucketOwner=DEFAULT_ACCOUNT_ID,
+        )
+
+    # Create inventory configurations for the second bucket
+    for i in range(2):
+        inventory_configuration = {
+            "Destination": {
+                "S3BucketDestination": {
+                    "AccountId": DEFAULT_ACCOUNT_ID,
+                    "Bucket": f"arn:aws:s3:::{bucket}",
+                    "Format": "CSV",
+                    "Prefix": "test",
+                    "Encryption": {"SSEKMS": {"KeyId": "key-12345"}},
+                }
+            },
+            "IsEnabled": True,
+            "Filter": {"Prefix": "folder1/"},
+            "Id": f"{id}-{bucket2}-{i}",
+            "IncludedObjectVersions": "All",
+            "OptionalFields": ["Size", "ETag"],
+            "Schedule": {"Frequency": "Daily"},
+        }
+        client.put_bucket_inventory_configuration(
+            Bucket=bucket2,
+            Id=id,
+            InventoryConfiguration=inventory_configuration,
+            ExpectedBucketOwner=DEFAULT_ACCOUNT_ID,
+        )
+
+    resp = client.list_bucket_inventory_configurations(Bucket=bucket)
+    assert len(resp["InventoryConfigurationList"]) == 3
+
+    resp = client.list_bucket_inventory_configurations(Bucket=bucket2)
+    assert len(resp["InventoryConfigurationList"]) == 2
+    assert resp["InventoryConfigurationList"][0]["Id"] == f"{id}-{bucket2}-0"
+    assert resp["InventoryConfigurationList"][1]["Id"] == f"{id}-{bucket2}-1"
+    assert (
+        resp["InventoryConfigurationList"][0]["Destination"]["S3BucketDestination"][
+            "AccountId"
+        ]
+        == DEFAULT_ACCOUNT_ID
+    )
diff --git a/tests/test_servicecatalogappregistry/__init__.py b/tests/test_servicecatalogappregistry/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_servicecatalogappregistry/test_servicecatalogappregistry.py b/tests/test_servicecatalogappregistry/test_servicecatalogappregistry.py
new file mode 100644
index 000000000..fc3016c3f
--- /dev/null
+++ b/tests/test_servicecatalogappregistry/test_servicecatalogappregistry.py
@@ -0,0 +1,131 @@
+"""Unit tests for servicecatalogappregistry-supported APIs."""
+
+import boto3
+import pytest
+from botocore.exceptions import ClientError
+
+from moto import mock_aws
+
+# See our Development Tips on writing tests for hints on how to write good tests:
+# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
+
+
+@mock_aws
+def test_create_application():
+    client = boto3.client("servicecatalog-appregistry", region_name="us-east-1")
+    resp = client.create_application(name="testapp", description="blah")
+
+    assert "id" in resp["application"]
+    assert resp["application"]["name"] == "testapp"
+    assert resp["application"]["description"] == "blah"
+
+
+@mock_aws
+def test_list_applications():
+    client = boto3.client("servicecatalog-appregistry", region_name="us-east-1")
+    client.create_application(name="testapp", description="blah")
+    client.create_application(name="testapp2", description="foo")
+    list_resp = client.list_applications()
+
+    assert "applications" in list_resp
+    assert list_resp["applications"][1]["name"] == "testapp2"
+    assert list_resp["applications"][1]["description"] == "foo"
+
+
+@mock_aws
+def test_associate_resource():
+    client = boto3.client("servicecatalog-appregistry", region_name="us-east-1")
+    create = client.create_application(name="testapp", description="blah")
+    resp = client.associate_resource(
+        application=create["application"]["id"],
+        resource="foo",
+        resourceType="RESOURCE_TAG_VALUE",
+        options=["APPLY_APPLICATION_TAG"],
+    )
+
+    assert "applicationArn" in resp
+    assert "resourceArn" in resp
+    assert "options" in resp
+    assert resp["applicationArn"] == create["application"]["arn"]
+
+
+@mock_aws
+def test_associate_resource_cloudformation():
+    client = boto3.client("servicecatalog-appregistry", region_name="us-east-1")
+    stack_name = "foo"
+    stack_id = boto3.client("cloudformation", region_name="us-east-1").create_stack(
+        StackName=stack_name,
+        TemplateBody="""
+---
+AWSTemplateFormatVersion: '2010-09-09'
+Resources:
+  S3:
+    Type: AWS::S3::Bucket
+    Properties:
+      BucketName: foo
+""",
+    )["StackId"]
+    create = client.create_application(name="testapp", description="blah")
+    resp = client.associate_resource(
+        application=create["application"]["id"],
+        resource=stack_id,
+        resourceType="CFN_STACK",
+        options=["APPLY_APPLICATION_TAG"],
+    )
+
+    assert "applicationArn" in resp
+    assert "resourceArn" in resp
+    assert "options" in resp
+    assert resp["applicationArn"] == create["application"]["arn"]
+
+
+@mock_aws
+def test_associate_resource_cloudformation_validation_error():
+    client = boto3.client("servicecatalog-appregistry", region_name="us-east-1")
+    create = client.create_application(name="testapp", description="blah")
+    stack_name = "foo"
+    with pytest.raises(ClientError) as exc:
+        client.associate_resource(
+            application=create["application"]["id"],
+            resource=stack_name,
+            resourceType="CFN_STACK",
+            options=["APPLY_APPLICATION_TAG"],
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ResourceNotFoundException"
+    assert err["Message"] == f"No CloudFormation stack called '{stack_name}' found"
+
+
+@mock_aws
+def test_associate_resource_fails_resource_type():
+    client = boto3.client("servicecatalog-appregistry", region_name="us-east-1")
+    create = client.create_application(name="testapp", description="blah")
+    with pytest.raises(ClientError) as exc:
+        client.associate_resource(
+            application=create["application"]["id"],
+            resource="foo",
+            resourceType="FOO",
+            options=["APPLY_APPLICATION_TAG"],
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationException"
+    assert err["Message"] == "The request has invalid or missing parameters."
+
+
+@mock_aws
+def test_list_associated_resources():
+    client = boto3.client("servicecatalog-appregistry", region_name="us-east-1")
+    create = client.create_application(name="testapp", description="blah")
+    associate = client.associate_resource(
+        application=create["application"]["id"],
+        resource="foo",
+        resourceType="RESOURCE_TAG_VALUE",
+        options=["APPLY_APPLICATION_TAG"],
+    )
+    resp = client.list_associated_resources(application=create["application"]["arn"])
+
+    assert "resources" in resp
+    assert "options" in resp["resources"][0]
+    assert "arn" in resp["resources"][0]
+    assert "resourceDetails" in resp["resources"][0]
+    assert resp["resources"][0]["options"] == associate["options"]
diff --git a/tests/test_timestreaminfluxdb/test_timestreaminfluxdb.py b/tests/test_timestreaminfluxdb/test_timestreaminfluxdb.py
index 8721ba2b7..c99d313ad 100644
--- a/tests/test_timestreaminfluxdb/test_timestreaminfluxdb.py
+++ b/tests/test_timestreaminfluxdb/test_timestreaminfluxdb.py
@@ -278,3 +278,245 @@ def test_list_db_instances():
     response = client.list_db_instances()
     resources = response["items"]
     assert len(resources) == 1
+
+
+@mock_aws
+def test_create_db_parameter_group():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+
+    response = client.create_db_parameter_group(
+        name="test-parameter-group",
+        description="Test parameter group for unit tests",
+        parameters={
+            "InfluxDBv2": {
+                "fluxLogEnabled": True,
+                "logLevel": "debug",
+                "queryQueueSize": 100,
+            }
+        },
+        tags={"Environment": "Test", "Project": "Moto"},
+    )
+
+    assert response["name"] == "test-parameter-group"
+    assert response["description"] == "Test parameter group for unit tests"
+    assert response["parameters"]["InfluxDBv2"]["fluxLogEnabled"] is True
+    assert response["parameters"]["InfluxDBv2"]["logLevel"] == "debug"
+    assert response["parameters"]["InfluxDBv2"]["queryQueueSize"] == 100
+    assert "id" in response
+    assert "arn" in response
+
+
+@mock_aws
+def test_get_db_parameter_group():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+
+    create_response = client.create_db_parameter_group(
+        name="test-parameter-group",
+        description="Test parameter group for unit tests",
+        parameters={"InfluxDBv2": {"fluxLogEnabled": True, "logLevel": "debug"}},
+    )
+
+    param_group_id = create_response["id"]
+
+    get_response = client.get_db_parameter_group(identifier=param_group_id)
+
+    assert get_response["id"] == param_group_id
+    assert get_response["name"] == "test-parameter-group"
+    assert get_response["description"] == "Test parameter group for unit tests"
+    assert get_response["parameters"]["InfluxDBv2"]["fluxLogEnabled"] is True
+    assert get_response["parameters"]["InfluxDBv2"]["logLevel"] == "debug"
+    assert "arn" in get_response
+
+    with pytest.raises(ClientError) as exc:
+        client.get_db_parameter_group(identifier="non-existent-id")
+
+    assert exc.value.response["Error"]["Code"] == "ResourceNotFoundException"
+
+
+@mock_aws
+def test_list_db_parameter_groups():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+
+    list_response = client.list_db_parameter_groups()
+    assert len(list_response["items"]) == 0
+    assert "nextToken" not in list_response
+
+    param_groups = []
+    for i in range(5):
+        response = client.create_db_parameter_group(
+            name=f"test-parameter-group-{i}",
+            description=f"Test parameter group {i}",
+            parameters={
+                "InfluxDBv2": {
+                    "fluxLogEnabled": i % 2 == 0,
+                    "logLevel": "debug" if i % 2 == 0 else "info",
+                }
+            },
+        )
+        param_groups.append(response)
+
+    list_response = client.list_db_parameter_groups()
+    assert len(list_response["items"]) == 5
+
+    list_response = client.list_db_parameter_groups(maxResults=2)
+    assert len(list_response["items"]) == 2
+    assert "nextToken" in list_response
+
+    next_token = list_response["nextToken"]
+    list_response = client.list_db_parameter_groups(nextToken=next_token, maxResults=2)
+    assert len(list_response["items"]) == 2
+    assert "nextToken" in list_response
+
+    next_token = list_response["nextToken"]
+    list_response = client.list_db_parameter_groups(nextToken=next_token, maxResults=2)
+    assert len(list_response["items"]) == 1
+    assert "nextToken" not in list_response
+
+
+@mock_aws
+def test_list_db_clusters():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+
+    list_response = client.list_db_clusters()
+    assert len(list_response["items"]) == 0
+    assert "nextToken" not in list_response
+
+    cluster_ids = []
+    for i in range(5):
+        response = client.create_db_cluster(
+            name=f"test-cluster-{i}",
+            password="password123",
+            dbInstanceType="db.influx.medium",
+            allocatedStorage=100,
+            vpcSubnetIds=["subnet-0123456789abcdef0", "subnet-0123456789abcdef1"],
+            vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+            deploymentType="MULTI_NODE_READ_REPLICAS",
+        )
+        cluster_ids.append(response["dbClusterId"])
+
+    list_response = client.list_db_clusters()
+    assert len(list_response["items"]) == 5
+
+    for item in list_response["items"]:
+        assert "id" in item
+        assert "name" in item
+        assert "arn" in item
+        assert "status" in item
+        assert "endpoint" in item
+        assert "readerEndpoint" in item
+        assert "port" in item
+        assert "deploymentType" in item
+        assert "dbInstanceType" in item
+        assert "networkType" in item
+        assert "dbStorageType" in item
+        assert "allocatedStorage" in item
+
+    list_response = client.list_db_clusters(maxResults=2)
+    assert len(list_response["items"]) == 2
+    assert "nextToken" in list_response
+
+    next_token = list_response["nextToken"]
+    list_response = client.list_db_clusters(nextToken=next_token, maxResults=2)
+    assert len(list_response["items"]) == 2
+    assert "nextToken" in list_response
+
+    next_token = list_response["nextToken"]
+    list_response = client.list_db_clusters(nextToken=next_token, maxResults=2)
+    assert len(list_response["items"]) == 1
+    assert "nextToken" not in list_response
+
+
+@mock_aws
+def test_get_db_cluster():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+
+    create_response = client.create_db_cluster(
+        name="test-cluster",
+        password="password123",
+        dbInstanceType="db.influx.medium",
+        allocatedStorage=100,
+        vpcSubnetIds=["subnet-0123456789abcdef0", "subnet-0123456789abcdef1"],
+        vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+        deploymentType="MULTI_NODE_READ_REPLICAS",
+    )
+
+    cluster_id = create_response["dbClusterId"]
+
+    get_response = client.get_db_cluster(dbClusterId=cluster_id)
+
+    assert get_response["id"] == cluster_id
+    assert get_response["name"] == "test-cluster"
+    assert get_response["status"] == "AVAILABLE"
+    assert get_response["deploymentType"] == "MULTI_NODE_READ_REPLICAS"
+    assert get_response["dbInstanceType"] == "db.influx.medium"
+    assert get_response["allocatedStorage"] == 100
+    assert isinstance(get_response["arn"], str)
+    assert isinstance(get_response["endpoint"], str)
+    assert isinstance(get_response["readerEndpoint"], str)
+    assert isinstance(get_response["port"], int)
+    assert isinstance(get_response["vpcSubnetIds"], list)
+    assert isinstance(get_response["vpcSecurityGroupIds"], list)
+    assert len(get_response["vpcSubnetIds"]) == 2
+    assert len(get_response["vpcSecurityGroupIds"]) == 1
+
+    with pytest.raises(ClientError) as exc:
+        client.get_db_cluster(dbClusterId="non-existent-id")
+
+    assert exc.value.response["Error"]["Code"] == "ResourceNotFoundException"
+
+
+@mock_aws
+def test_create_db_cluster():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+
+    response = client.create_db_cluster(
+        name="test-cluster",
+        password="password123",
+        dbInstanceType="db.influx.medium",
+        allocatedStorage=100,
+        vpcSubnetIds=["subnet-0123456789abcdef0", "subnet-0123456789abcdef1"],
+        vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+        deploymentType="MULTI_NODE_READ_REPLICAS",
+    )
+
+    assert "dbClusterId" in response
+    assert response["dbClusterStatus"] == "AVAILABLE"
+
+    with pytest.raises(ClientError) as exc:
+        client.create_db_cluster(
+            name="test-cluster",
+            password="password123",
+            dbInstanceType="db.influx.medium",
+            allocatedStorage=100,
+            vpcSubnetIds=["subnet-0123456789abcdef0", "subnet-0123456789abcdef1"],
+            vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+            deploymentType="MULTI_NODE_READ_REPLICAS",
+        )
+
+    assert exc.value.response["Error"]["Code"] == "ConflictException"
+
+    response = client.create_db_cluster(
+        name="test-cluster-full",
+        username="admin",
+        password="password123",
+        organization="test-org",
+        bucket="test-bucket",
+        port=8088,
+        dbParameterGroupIdentifier="test-param-group",
+        dbInstanceType="db.influx.medium",
+        dbStorageType="InfluxIOIncludedT1",
+        allocatedStorage=100,
+        networkType="IPV4",
+        publiclyAccessible=True,
+        vpcSubnetIds=["subnet-0123456789abcdef0", "subnet-0123456789abcdef1"],
+        vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+        deploymentType="MULTI_NODE_READ_REPLICAS",
+        failoverMode="AUTOMATIC",
+        logDeliveryConfiguration={
+            "s3Configuration": {"bucketName": "test-bucket", "enabled": True}
+        },
+        tags={"Environment": "Test", "Project": "Moto"},
+    )
+
+    assert "dbClusterId" in response
+    assert response["dbClusterStatus"] == "AVAILABLE"