Repository: getmoto/moto
Pull request #7959 (2024-08-09 14:54:44)

CloudFormation: create_change_set ChangeSetName more rules
### Pull Request: Refactor Validation in `create_change_set`

**Summary:**

This PR refactors the `create_change_set` function to improve validation by centralizing it in a new `validate_create_change_set` function. This change ensures all parameters are validated consistently and makes it easier to extend validation in the future.

**Changes:**

- **Added `validate_create_change_set` Function**:
  - Centralizes validation for all parameters, currently including `change_set_name` rules (e.g., must start with a letter, be alphanumeric, and under 128 characters).

- **Updated `create_change_set`**:
  - Calls `validate_create_change_set` to validate inputs before processing.

**Testing:**

- Updated unit tests to cover validation scenarios.

[Documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudformation/client/create_change_set.html#:~:text=A%20change%20set%20name%20can%20contain%20only%20alphanumeric%2C%20case%20sensitive%20characters%2C%20and%20hyphens.%20It%20must%20start%20with%20an%20alphabetical%20character%20and%20can%E2%80%99t%20exceed%20128%20characters.)

![image](https://github.com/user-attachments/assets/c3890eb9-b818-4802-b45d-1c5877c3091f)

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7959?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 94.36%. Comparing base [(`59bff90`)](https://app.codecov.io/gh/getmoto/moto/commit/59bff902f09871c775a8a866e37c094f015057d4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`ef3c5c4`)](https://app.codecov.io/gh/getmoto/moto/commit/ef3c5c4b28896f908284f468deb8ecb9fffee51b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 80 commits behind head on master.

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


```diff
@@            Coverage Diff             @@
##           master    #7959      +/-   ##
==========================================
+ Coverage   94.34%   94.36%   +0.01%     
==========================================
  Files        1113     1120       +7     
  Lines       94413    96074    +1661     
==========================================
+ Hits        89076    90660    +1584     
- Misses       5337     5414      +77     
```

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

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

</details>

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

tinngo365: @bblommers could you explain what is wrong?

Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index a621e2919..b42e4b90b 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -4773,11 +4773,11 @@
 
 ## kms
 <details>
-<summary>56% implemented</summary>
+<summary>64% implemented</summary>
 
 - [X] cancel_key_deletion
 - [ ] connect_custom_key_store
-- [ ] create_alias
+- [X] create_alias
 - [ ] create_custom_key_store
 - [X] create_grant
 - [X] create_key
@@ -4797,7 +4797,7 @@
 - [X] generate_data_key
 - [ ] generate_data_key_pair
 - [ ] generate_data_key_pair_without_plaintext
-- [ ] generate_data_key_without_plaintext
+- [X] generate_data_key_without_plaintext
 - [ ] generate_mac
 - [ ] generate_random
 - [X] get_key_policy
@@ -4805,7 +4805,7 @@
 - [ ] get_parameters_for_import
 - [X] get_public_key
 - [ ] import_key_material
-- [ ] list_aliases
+- [X] list_aliases
 - [X] list_grants
 - [ ] list_key_policies
 - [ ] list_key_rotations
@@ -4822,7 +4822,7 @@
 - [X] sign
 - [X] tag_resource
 - [X] untag_resource
-- [ ] update_alias
+- [X] update_alias
 - [ ] update_custom_key_store
 - [X] update_key_description
 - [ ] update_primary_region
diff --git a/docs/docs/services/kms.rst b/docs/docs/services/kms.rst
index d1a774a34..4294fddcd 100644
--- a/docs/docs/services/kms.rst
+++ b/docs/docs/services/kms.rst
@@ -16,7 +16,7 @@ kms
 
 - [X] cancel_key_deletion
 - [ ] connect_custom_key_store
-- [ ] create_alias
+- [X] create_alias
 - [ ] create_custom_key_store
 - [X] create_grant
 - [X] create_key
@@ -47,7 +47,7 @@ kms
 - [X] generate_data_key
 - [ ] generate_data_key_pair
 - [ ] generate_data_key_pair_without_plaintext
-- [ ] generate_data_key_without_plaintext
+- [X] generate_data_key_without_plaintext
 - [ ] generate_mac
 - [ ] generate_random
 - [X] get_key_policy
@@ -55,7 +55,7 @@ kms
 - [ ] get_parameters_for_import
 - [X] get_public_key
 - [ ] import_key_material
-- [ ] list_aliases
+- [X] list_aliases
 - [X] list_grants
 - [ ] list_key_policies
 - [ ] list_key_rotations
@@ -78,7 +78,7 @@ kms
 
 - [X] tag_resource
 - [X] untag_resource
-- [ ] update_alias
+- [X] update_alias
 - [ ] update_custom_key_store
 - [X] update_key_description
 - [ ] update_primary_region
diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py
index 6d5c6bee5..08ce7c7d7 100644
--- a/moto/cloudformation/models.py
+++ b/moto/cloudformation/models.py
@@ -28,6 +28,7 @@ from .utils import (
     generate_stackset_arn,
     generate_stackset_id,
     get_stack_from_s3_url,
+    validate_create_change_set,
     validate_template_cfn_lint,
     yaml_tag_constructor,
 )
@@ -1007,6 +1008,7 @@ class CloudFormationBackend(BaseBackend):
         tags: Optional[Dict[str, str]] = None,
         role_arn: Optional[str] = None,
     ) -> Tuple[str, str]:
+        validate_create_change_set(change_set_name)
         if change_set_type == "UPDATE":
             for stack in self.stacks.values():
                 if stack.name == stack_name:
diff --git a/moto/cloudformation/utils.py b/moto/cloudformation/utils.py
index af47a4068..598f8dc7c 100644
--- a/moto/cloudformation/utils.py
+++ b/moto/cloudformation/utils.py
@@ -8,6 +8,8 @@ import yaml
 from moto.moto_api._internal import mock_random as random
 from moto.utilities.utils import get_partition
 
+from .exceptions import ValidationError
+
 
 def generate_stack_id(stack_name: str, region: str, account: str) -> str:
     random_id = random.uuid4()
@@ -134,3 +136,19 @@ def get_stack_from_s3_url(template_url: str, account_id: str, partition: str) ->
 
     key = s3_backends[account_id][partition].get_object(bucket_name, key_name)
     return key.value.decode("utf-8")  # type: ignore[union-attr]
+
+
+def validate_create_change_set(change_set_name: str) -> None:
+    if not (change_set_name and change_set_name[0].isalpha()):
+        raise ValidationError(f"Invalid change set name: {change_set_name}")
+
+    if not all(c.isalnum() or c == "-" for c in change_set_name):
+        raise ValidationError(f"Invalid change set name: {change_set_name}")
+
+    if len(change_set_name) > 128:
+        raise ValidationError(
+            f"Change set name exceeds 128 characters: {change_set_name}"
+        )
+
+    # Additional validations can be added here later
+    return
diff --git a/moto/codebuild/models.py b/moto/codebuild/models.py
index 34b96311d..2c0de3445 100644
--- a/moto/codebuild/models.py
+++ b/moto/codebuild/models.py
@@ -6,7 +6,7 @@ from dateutil import parser
 
 from moto.core.base_backend import BackendDict, BaseBackend
 from moto.core.common_models import BaseModel
-from moto.core.utils import iso_8601_datetime_with_milliseconds
+from moto.core.utils import iso_8601_datetime_with_milliseconds, unix_time
 from moto.moto_api._internal import mock_random
 from moto.utilities.utils import get_partition
 
@@ -98,24 +98,38 @@ class CodeBuild(BaseModel):
         account_id: str,
         region: str,
         project_name: str,
+        description: Optional[str],
         project_source: Dict[str, Any],
         artifacts: Dict[str, Any],
         environment: Dict[str, Any],
         serviceRole: str = "some_role",
+        tags: Optional[List[Dict[str, str]]] = None,
+        cache: Optional[Dict[str, Any]] = None,
+        timeout: Optional[int] = 0,
+        queued_timeout: Optional[int] = 0,
+        source_version: Optional[str] = None,
+        logs_config: Optional[Dict[str, Any]] = None,
+        vpc_config: Optional[Dict[str, Any]] = None,
     ):
-        current_date = iso_8601_datetime_with_milliseconds()
+        self.arn = f"arn:{get_partition(region)}:codebuild:{region}:{account_id}:project/{project_name}"
+        self.service_role = serviceRole
+        self.tags = tags
+        current_date = unix_time()
         self.project_metadata: Dict[str, Any] = dict()
 
         self.project_metadata["name"] = project_name
-        self.project_metadata["arn"] = (
-            f"arn:{get_partition(region)}:codebuild:{region}:{account_id}:project/{project_name}"
-        )
+        if description:
+            self.project_metadata["description"] = description
+        self.project_metadata["arn"] = self.arn
         self.project_metadata["encryptionKey"] = (
             f"arn:{get_partition(region)}:kms:{region}:{account_id}:alias/aws/s3"
         )
-        self.project_metadata["serviceRole"] = (
-            f"arn:{get_partition(region)}:iam::{account_id}:role/service-role/{serviceRole}"
-        )
+        if serviceRole.startswith("arn:"):
+            self.project_metadata["serviceRole"] = serviceRole
+        else:
+            self.project_metadata["serviceRole"] = (
+                f"arn:{get_partition(region)}:iam::{account_id}:role/service-role/{serviceRole}"
+            )
         self.project_metadata["lastModifiedDate"] = current_date
         self.project_metadata["created"] = current_date
         self.project_metadata["badge"] = dict()
@@ -125,10 +139,16 @@ class CodeBuild(BaseModel):
         self.project_metadata["environment"] = environment
         self.project_metadata["artifacts"] = artifacts
         self.project_metadata["source"] = project_source
-        self.project_metadata["cache"] = dict()
-        self.project_metadata["cache"]["type"] = "NO_CACHE"
-        self.project_metadata["timeoutInMinutes"] = ""
-        self.project_metadata["queuedTimeoutInMinutes"] = ""
+        self.project_metadata["cache"] = cache or {"type": "NO_CACHE"}
+        self.project_metadata["timeoutInMinutes"] = timeout or 0
+        self.project_metadata["queuedTimeoutInMinutes"] = queued_timeout or 0
+        self.project_metadata["tags"] = tags
+        if source_version:
+            self.project_metadata["sourceVersion"] = source_version
+        if logs_config:
+            self.project_metadata["logsConfig"] = logs_config
+        if vpc_config:
+            self.project_metadata["vpcConfig"] = vpc_config
 
 
 class CodeBuildBackend(BaseBackend):
@@ -142,23 +162,35 @@ class CodeBuildBackend(BaseBackend):
     def create_project(
         self,
         project_name: str,
+        description: Optional[str],
         project_source: Dict[str, Any],
         artifacts: Dict[str, Any],
         environment: Dict[str, Any],
         service_role: str,
+        tags: Optional[List[Dict[str, str]]],
+        cache: Optional[Dict[str, Any]],
+        timeout: Optional[int],
+        queued_timeout: Optional[int],
+        source_version: Optional[str],
+        logs_config: Optional[Dict[str, Any]],
+        vpc_config: Optional[Dict[str, Any]],
     ) -> Dict[str, Any]:
-        # required in other functions that don't
-        self.project_name = project_name
-        self.service_role = service_role
-
         self.codebuild_projects[project_name] = CodeBuild(
             self.account_id,
             self.region_name,
-            project_name,
-            project_source,
-            artifacts,
-            environment,
-            service_role,
+            project_name=project_name,
+            description=description,
+            project_source=project_source,
+            artifacts=artifacts,
+            environment=environment,
+            serviceRole=service_role,
+            tags=tags,
+            cache=cache,
+            timeout=timeout,
+            queued_timeout=queued_timeout,
+            source_version=source_version,
+            logs_config=logs_config,
+            vpc_config=vpc_config,
         )
 
         # empty build history
@@ -174,12 +206,24 @@ class CodeBuildBackend(BaseBackend):
 
         return projects
 
+    def batch_get_projects(self, names: List[str]) -> List[Dict[str, Any]]:
+        result = []
+        for name in names:
+            if name in self.codebuild_projects:
+                result.append(self.codebuild_projects[name].project_metadata)
+            elif name.startswith("arn:"):
+                for project in self.codebuild_projects.values():
+                    if name == project.arn:
+                        result.append(project.project_metadata)
+        return result
+
     def start_build(
         self,
         project_name: str,
         source_version: Optional[str] = None,
         artifact_override: Optional[Dict[str, Any]] = None,
     ) -> Dict[str, Any]:
+        project = self.codebuild_projects[project_name]
         build_id = f"{project_name}:{mock_random.uuid4()}"
 
         # construct a new build
@@ -190,7 +234,7 @@ class CodeBuildBackend(BaseBackend):
             source_version,
             artifact_override,
             build_id,
-            self.service_role,
+            project.service_role,
         )
 
         self.build_history[project_name].append(build_id)
diff --git a/moto/codebuild/responses.py b/moto/codebuild/responses.py
index f516f6c2c..f8bba680e 100644
--- a/moto/codebuild/responses.py
+++ b/moto/codebuild/responses.py
@@ -136,11 +136,19 @@ class CodeBuildResponse(BaseResponse):
             )
 
         project_metadata = self.codebuild_backend.create_project(
-            self._get_param("name"),
-            self._get_param("source"),
-            self._get_param("artifacts"),
-            self._get_param("environment"),
+            project_name=self._get_param("name"),
+            description=self._get_param("description"),
+            project_source=self._get_param("source"),
+            artifacts=self._get_param("artifacts"),
+            environment=self._get_param("environment"),
             service_role=service_role,
+            tags=self._get_param("tags"),
+            cache=self._get_param("cache"),
+            timeout=self._get_param("timeoutInMinutes"),
+            queued_timeout=self._get_param("queuedTimeoutInMinutes"),
+            source_version=self._get_param("sourceVersion"),
+            logs_config=self._get_param("logsConfig"),
+            vpc_config=self._get_param("vpcConfig"),
         )
 
         return json.dumps({"project": project_metadata})
@@ -149,6 +157,11 @@ class CodeBuildResponse(BaseResponse):
         project_metadata = self.codebuild_backend.list_projects()
         return json.dumps({"projects": project_metadata})
 
+    def batch_get_projects(self) -> str:
+        names = self._get_param("names")
+        project_metadata = self.codebuild_backend.batch_get_projects(names)
+        return json.dumps({"projects": project_metadata})
+
     def start_build(self) -> str:
         _validate_required_params_project_name(self._get_param("projectName"))
 
diff --git a/moto/ds/models.py b/moto/ds/models.py
index ec1945e38..0145259b8 100644
--- a/moto/ds/models.py
+++ b/moto/ds/models.py
@@ -1,10 +1,10 @@
 """DirectoryServiceBackend class with methods for supported APIs."""
 
-from datetime import datetime, timezone
 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.core.utils import unix_time
 from moto.ds.exceptions import (
     ClientException,
     DirectoryLimitExceededException,
@@ -80,8 +80,8 @@ class Directory(BaseModel):  # pylint: disable=too-many-instance-attributes
         self.desired_number_of_domain_controllers = 0
         self.sso_enabled = False
         self.stage = "Active"
-        self.launch_time = datetime.now(timezone.utc).isoformat()
-        self.stage_last_updated_date_time = datetime.now(timezone.utc).isoformat()
+        self.launch_time = unix_time()
+        self.stage_last_updated_date_time = unix_time()
 
         if self.directory_type == "ADConnector":
             self.security_group_id = self.create_security_group(
diff --git a/moto/dynamodb/models/table.py b/moto/dynamodb/models/table.py
index 244fb2fa4..8e8d9f217 100644
--- a/moto/dynamodb/models/table.py
+++ b/moto/dynamodb/models/table.py
@@ -324,7 +324,7 @@ class Table(CloudFormationModel):
                 description="Default master key that protects my DynamoDB table storage",
                 tags=None,
             )
-            kms.add_alias(key.id, ddb_alias)
+            kms.create_alias(key.id, ddb_alias)
         ebs_key = kms.describe_key(ddb_alias)
         return ebs_key.arn
 
diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py
index d77ed7cdf..9d000ffbf 100644
--- a/moto/dynamodb/responses.py
+++ b/moto/dynamodb/responses.py
@@ -965,7 +965,11 @@ class DynamoHandler(BaseResponse):
         return dynamo_json_dump(item_dict)
 
     def _get_expr_attr_values(self) -> Dict[str, Dict[str, str]]:
-        values = self.body.get("ExpressionAttributeValues", {})
+        values = self.body.get("ExpressionAttributeValues")
+        if values is None:
+            return {}
+        if len(values) == 0:
+            raise MockValidationException("ExpressionAttributeValues must not be empty")
         for key in values.keys():
             if not key.startswith(":"):
                 raise MockValidationException(
diff --git a/moto/ec2/models/elastic_block_store.py b/moto/ec2/models/elastic_block_store.py
index d820401f9..10b87bf26 100644
--- a/moto/ec2/models/elastic_block_store.py
+++ b/moto/ec2/models/elastic_block_store.py
@@ -547,6 +547,6 @@ class EBSBackend:
                 description="Default master key that protects my EBS volumes when no other key is defined",
                 tags=None,
             )
-            kms.add_alias(key.id, ebs_alias)
+            kms.create_alias(key.id, ebs_alias)
         ebs_key = kms.describe_key(ebs_alias)
         return ebs_key.arn
diff --git a/moto/ec2/models/security_groups.py b/moto/ec2/models/security_groups.py
index 869c47d98..eac8cf5f2 100644
--- a/moto/ec2/models/security_groups.py
+++ b/moto/ec2/models/security_groups.py
@@ -517,6 +517,49 @@ class SecurityGroup(TaggedEC2Resource, CloudFormationModel):
             len(rule.ip_ranges) + len(rule.source_groups) for rule in self.egress_rules
         )
 
+    @property
+    def flattened_ingress_rules(self) -> List[SecurityRule]:
+        return self._flattened_rules(copy.copy(self.ingress_rules))
+
+    @property
+    def flattened_egress_rules(self) -> List[SecurityRule]:
+        return self._flattened_rules(copy.copy(self.egress_rules))
+
+    def _flattened_rules(self, rules: List[SecurityRule]) -> List[SecurityRule]:
+        rules_to_return: List[SecurityRule] = []
+        for rule in rules:
+            for already_added in rules_to_return:
+                if (
+                    already_added.from_port == rule.from_port
+                    and already_added.to_port == rule.to_port
+                    and already_added.ip_protocol == rule.ip_protocol
+                ):
+                    already_added.ip_ranges.extend(
+                        [
+                            item
+                            for item in rule.ip_ranges
+                            if item not in already_added.ip_ranges
+                        ]
+                    )
+                    already_added.source_groups.extend(
+                        [
+                            item
+                            for item in rule.source_groups
+                            if item not in already_added.source_groups
+                        ]
+                    )
+                    already_added.prefix_list_ids.extend(
+                        [
+                            item
+                            for item in rule.prefix_list_ids
+                            if item not in already_added.prefix_list_ids
+                        ]
+                    )
+                    break
+            else:
+                rules_to_return.append(rule)
+        return rules_to_return
+
 
 class SecurityGroupBackend:
     def __init__(self) -> None:
@@ -767,6 +810,8 @@ class SecurityGroupBackend:
                 security_rule.from_port == rule.from_port
                 and security_rule.to_port == rule.to_port
                 and security_rule.ip_protocol == rule.ip_protocol
+                and security_rule.ip_ranges == rule.ip_ranges
+                and security_rule.prefix_list_ids == rule.prefix_list_ids
             ):
                 rule.ip_ranges.extend(
                     [
@@ -941,6 +986,8 @@ class SecurityGroupBackend:
                 security_rule.from_port == rule.from_port
                 and security_rule.to_port == rule.to_port
                 and security_rule.ip_protocol == rule.ip_protocol
+                and security_rule.ip_ranges == rule.ip_ranges
+                and security_rule.prefix_list_ids == rule.prefix_list_ids
             ):
                 rule.ip_ranges.extend(
                     [
diff --git a/moto/ec2/responses/security_groups.py b/moto/ec2/responses/security_groups.py
index cf1db0b2b..d71b88b9d 100644
--- a/moto/ec2/responses/security_groups.py
+++ b/moto/ec2/responses/security_groups.py
@@ -280,13 +280,19 @@ DESCRIBE_SECURITY_GROUP_RULES_RESPONSE = """
             <item>
                 <fromPort>{{ rule.from_port if rule.from_port is not none else -1 }}</fromPort>
                 <toPort>{{ rule.to_port if rule.to_port is not none else -1 }}</toPort>
-                <cidrIpv4>{{ ip_range['CidrIp'] }}</cidrIpv4>
+                {% if ip_range['CidrIp'] %}<cidrIpv4>{{ ip_range['CidrIp'] }}</cidrIpv4>{% endif %}
+                {% if ip_range['CidrIpv6'] %}<cidrIpv6>{{ ip_range['CidrIpv6'] }}</cidrIpv6>{% endif %}
                 <ipProtocol>{{ rule.ip_protocol }}</ipProtocol>
                 <groupId>{{ rule.group_id }}</groupId>
                 <groupOwnerId>{{ rule.owner_id }}</groupOwnerId>
-                <description>{{ ip_range['Description'] }}</description>
+                {% if ip_range['Description'] %}<description>{{ ip_range['Description'] }}</description>{% endif %}
                 <isEgress>{{ 'true' if rule.is_egress else 'false' }}</isEgress>
                 <securityGroupRuleId>{{ rule.id }}</securityGroupRuleId>
+                {% if (rule.source_groups | length) > 0 %}
+                <referencedGroupInfo>
+                  <groupId>{{ rule.source_groups[0]['GroupId'] }}</groupId><userId>{{ rule.source_groups[0]['OwnerId'] }}</userId>
+                </referencedGroupInfo>
+                {% endif %}
                 <tagSet>
                 {% for tag in rule.get_tags() %}
                     <item>
@@ -297,6 +303,31 @@ DESCRIBE_SECURITY_GROUP_RULES_RESPONSE = """
                 </tagSet> 
             </item>
           {% endfor %}
+          {% if rule.ip_ranges | length == 0 %}
+            <item>
+                <fromPort>{{ rule.from_port if rule.from_port is not none else -1 }}</fromPort>
+                <toPort>{{ rule.to_port if rule.to_port is not none else -1 }}</toPort>
+                <ipProtocol>{{ rule.ip_protocol }}</ipProtocol>
+                {% if rule.prefix_list_ids | length > 0 %}<prefixListId>{{ rule.prefix_list_ids[0]['PrefixListId'] }}</prefixListId>{% endif %}
+                <groupId>{{ rule.group_id }}</groupId>
+                <groupOwnerId>{{ rule.owner_id }}</groupOwnerId>
+                <isEgress>{{ 'true' if rule.is_egress else 'false' }}</isEgress>
+                <securityGroupRuleId>{{ rule.id }}</securityGroupRuleId>
+                {% if (rule.source_groups | length) > 0 %}
+                <referencedGroupInfo>
+                  <groupId>{{ rule.source_groups[0]['GroupId'] }}</groupId><userId>{{ rule.source_groups[0]['OwnerId'] }}</userId>
+                </referencedGroupInfo>
+                {% endif %}
+                <tagSet>
+                {% for tag in rule.get_tags() %}
+                    <item>
+                      <key>{{ tag.key }}</key>
+                      <value>{{ tag.value }}</value>
+                    </item>
+                {% endfor %}
+                </tagSet> 
+            </item>
+          {% endif %}
         {% endfor %}
   </securityGroupRuleSet>
 </DescribeSecurityGroupRulesResponse>"""
@@ -319,7 +350,7 @@ DESCRIBE_SECURITY_GROUPS_RESPONSE = """<DescribeSecurityGroupsResponse xmlns="ht
              <vpcId>{{ group.vpc_id }}</vpcId>
              {% endif %}
              <ipPermissions>
-               {% for rule in group.ingress_rules %}
+               {% for rule in group.flattened_ingress_rules %}
                     <item>
                        <ipProtocol>{{ rule.ip_protocol }}</ipProtocol>
                        {% if rule.from_port is not none %}
@@ -384,7 +415,7 @@ DESCRIBE_SECURITY_GROUPS_RESPONSE = """<DescribeSecurityGroupsResponse xmlns="ht
                 {% endfor %}
              </ipPermissions>
              <ipPermissionsEgress>
-               {% for rule in group.egress_rules %}
+               {% for rule in group.flattened_egress_rules %}
                     <item>
                        <ipProtocol>{{ rule.ip_protocol }}</ipProtocol>
                        {% if rule.from_port is not none %}
diff --git a/moto/kms/models.py b/moto/kms/models.py
index aed7cec72..9f35cd893 100644
--- a/moto/kms/models.py
+++ b/moto/kms/models.py
@@ -288,7 +288,7 @@ class KmsBackend(BaseBackend):
                 "Default key",
                 None,
             )
-            self.add_alias(key.id, alias_name)
+            self.create_alias(key.id, alias_name)
             return key.id
         return None
 
@@ -430,17 +430,20 @@ class KmsBackend(BaseBackend):
 
         return False
 
-    def add_alias(self, target_key_id: str, alias_name: str) -> None:
+    def create_alias(self, target_key_id: str, alias_name: str) -> None:
         raw_key_id = self.get_key_id(target_key_id)
         self.key_to_aliases[raw_key_id].add(alias_name)
 
+    def update_alias(self, target_key_id: str, alias_name: str) -> None:
+        self.create_alias(target_key_id, alias_name)
+
     def delete_alias(self, alias_name: str) -> None:
         """Delete the alias."""
         for aliases in self.key_to_aliases.values():
             if alias_name in aliases:
                 aliases.remove(alias_name)
 
-    def get_all_aliases(self) -> Dict[str, Set[str]]:
+    def list_aliases(self) -> Dict[str, Set[str]]:
         return self.key_to_aliases
 
     def get_key_id_from_alias(self, alias_name: str) -> Optional[str]:
@@ -558,6 +561,11 @@ class KmsBackend(BaseBackend):
 
         return plaintext, ciphertext_blob, arn
 
+    def generate_data_key_without_plaintext(self) -> None:
+        # Marker to indicate this is implemented
+        # Responses uses 'generate_data_key'
+        pass
+
     def list_resource_tags(self, key_id_or_arn: str) -> Dict[str, List[Dict[str, str]]]:
         key_id = self.get_key_id(key_id_or_arn)
         if key_id in self.keys:
diff --git a/moto/kms/responses.py b/moto/kms/responses.py
index 506a9404f..b68ec20b0 100644
--- a/moto/kms/responses.py
+++ b/moto/kms/responses.py
@@ -246,7 +246,10 @@ class KmsResponse(BaseResponse):
             )
 
         self._validate_cmk_id(target_key_id)
-        self.kms_backend.add_alias(target_key_id, alias_name)
+        if update:
+            self.kms_backend.update_alias(target_key_id, alias_name)
+        else:
+            self.kms_backend.create_alias(target_key_id, alias_name)
 
         return json.dumps(None)
 
@@ -273,7 +276,7 @@ class KmsResponse(BaseResponse):
 
         response_aliases = []
 
-        backend_aliases = self.kms_backend.get_all_aliases()
+        backend_aliases = self.kms_backend.list_aliases()
         for target_key_id, aliases in backend_aliases.items():
             for alias_name in aliases:
                 # TODO: add creation date and last updated in response_aliases
diff --git a/moto/organizations/models.py b/moto/organizations/models.py
index eacdcdee6..2f0f03b18 100644
--- a/moto/organizations/models.py
+++ b/moto/organizations/models.py
@@ -88,6 +88,35 @@ class FakeAccount(BaseModel):
         self.attached_policies: List[FakePolicy] = []
         self.tags = {tag["Key"]: tag["Value"] for tag in kwargs.get("Tags", [])}
 
+        role_name = kwargs.get("RoleName", "OrganizationAccountAccessRole")
+
+        from moto.iam import iam_backends
+        from moto.iam.exceptions import EntityAlreadyExists
+
+        trust_policy = {
+            "Version": "2012-10-17",
+            "Statement": [
+                {
+                    "Effect": "Allow",
+                    "Principal": {"AWS": f"arn:aws:iam::{self.master_account_id}:root"},
+                    "Action": "sts:AssumeRole",
+                }
+            ],
+        }
+        iam = iam_backends[self.id]["global"]
+        try:
+            iam.create_role(
+                role_name=role_name,
+                assume_role_policy_document=json.dumps(trust_policy),
+                path="",
+                permissions_boundary=None,
+                description="",
+                tags=[],
+                max_session_duration="3600",
+            )
+        except EntityAlreadyExists:
+            pass
+
     @property
     def arn(self) -> str:
         partition = get_partition(self.region)
diff --git a/moto/transfer/models.py b/moto/transfer/models.py
index 360b82549..ca9f5e69e 100644
--- a/moto/transfer/models.py
+++ b/moto/transfer/models.py
@@ -131,7 +131,7 @@ class TransferBackend(BaseBackend):
         self,
         home_directory: Optional[str],
         home_directory_type: Optional[UserHomeDirectoryType],
-        home_directory_mappings: List[Dict[str, Optional[str]]],
+        home_directory_mappings: Optional[List[Dict[str, Optional[str]]]],
         policy: Optional[str],
         posix_profile: Optional[Dict[str, Any]],
         role: str,
@@ -150,7 +150,7 @@ class TransferBackend(BaseBackend):
             tags=(tags or []),
             user_name=user_name,
         )
-        if len(home_directory_mappings) > 0:
+        if home_directory_mappings:
             for mapping in home_directory_mappings:
                 user.home_directory_mappings.append(
                     {
diff --git a/other_langs/terraform/codebuild/project.tf b/other_langs/terraform/codebuild/project.tf
new file mode 100644
index 000000000..a244165c7
--- /dev/null
+++ b/other_langs/terraform/codebuild/project.tf
@@ -0,0 +1,234 @@
+resource "aws_vpc" "example" {
+  cidr_block = "10.0.0.0/16"
+}
+
+resource "aws_subnet" "example1" {
+  vpc_id     = aws_vpc.example.id
+  cidr_block = "10.0.1.0/24"
+}
+
+resource "aws_subnet" "example2" {
+  vpc_id     = aws_vpc.example.id
+  cidr_block = "10.0.2.0/24"
+}
+
+resource "aws_security_group" "example1" {
+  name        = "sg name"
+  description = "Desc"
+  vpc_id      = aws_vpc.example.id
+}
+
+resource "aws_security_group" "example2" {
+  name        = "sg name 2"
+  description = "Desc"
+  vpc_id      = aws_vpc.example.id
+}
+
+resource "aws_s3_bucket" "example" {
+  bucket = "example"
+}
+
+resource "aws_s3_bucket_acl" "example" {
+  bucket = aws_s3_bucket.example.id
+  acl    = "private"
+}
+
+data "aws_iam_policy_document" "assume_role" {
+  statement {
+    effect = "Allow"
+
+    principals {
+      type        = "Service"
+      identifiers = ["codebuild.amazonaws.com"]
+    }
+
+    actions = ["sts:AssumeRole"]
+  }
+}
+
+resource "aws_iam_role" "example" {
+  name               = "example"
+  assume_role_policy = data.aws_iam_policy_document.assume_role.json
+}
+
+data "aws_iam_policy_document" "example" {
+  statement {
+    effect = "Allow"
+
+    actions = [
+      "logs:CreateLogGroup",
+      "logs:CreateLogStream",
+      "logs:PutLogEvents",
+    ]
+
+    resources = ["*"]
+  }
+
+  statement {
+    effect = "Allow"
+
+    actions = [
+      "ec2:CreateNetworkInterface",
+      "ec2:DescribeDhcpOptions",
+      "ec2:DescribeNetworkInterfaces",
+      "ec2:DeleteNetworkInterface",
+      "ec2:DescribeSubnets",
+      "ec2:DescribeSecurityGroups",
+      "ec2:DescribeVpcs",
+    ]
+
+    resources = ["*"]
+  }
+
+  statement {
+    effect    = "Allow"
+    actions   = ["ec2:CreateNetworkInterfacePermission"]
+    resources = ["arn:aws:ec2:us-east-1:123456789012:network-interface/*"]
+
+    condition {
+      test     = "StringEquals"
+      variable = "ec2:Subnet"
+
+      values = [
+        aws_subnet.example1.arn,
+        aws_subnet.example2.arn,
+      ]
+    }
+
+    condition {
+      test     = "StringEquals"
+      variable = "ec2:AuthorizedService"
+      values   = ["codebuild.amazonaws.com"]
+    }
+  }
+
+  statement {
+    effect  = "Allow"
+    actions = ["s3:*"]
+    resources = [
+      aws_s3_bucket.example.arn,
+      "${aws_s3_bucket.example.arn}/*",
+    ]
+  }
+}
+
+resource "aws_iam_role_policy" "example" {
+  role   = aws_iam_role.example.name
+  policy = data.aws_iam_policy_document.example.json
+}
+
+resource "aws_codebuild_project" "example" {
+  name          = "test-project"
+  description   = "test_codebuild_project"
+  build_timeout = 5
+  service_role  = aws_iam_role.example.arn
+
+  artifacts {
+    type = "NO_ARTIFACTS"
+  }
+
+  cache {
+    type     = "S3"
+    location = aws_s3_bucket.example.bucket
+  }
+
+  environment {
+    compute_type                = "BUILD_GENERAL1_SMALL"
+    image                       = "aws/codebuild/amazonlinux2-x86_64-standard:4.0"
+    type                        = "LINUX_CONTAINER"
+    image_pull_credentials_type = "CODEBUILD"
+
+    environment_variable {
+      name  = "SOME_KEY1"
+      value = "SOME_VALUE1"
+    }
+
+    environment_variable {
+      name  = "SOME_KEY2"
+      value = "SOME_VALUE2"
+      type  = "PARAMETER_STORE"
+    }
+  }
+
+  logs_config {
+    cloudwatch_logs {
+      group_name  = "log-group"
+      stream_name = "log-stream"
+    }
+
+    s3_logs {
+      status   = "ENABLED"
+      location = "${aws_s3_bucket.example.id}/build-log"
+    }
+  }
+
+  source {
+    type            = "GITHUB"
+    location        = "https://github.com/mitchellh/packer.git"
+    git_clone_depth = 1
+
+    git_submodules_config {
+      fetch_submodules = true
+    }
+  }
+
+  source_version = "master"
+
+  vpc_config {
+    vpc_id = aws_vpc.example.id
+
+    subnets = [
+      aws_subnet.example1.id,
+      aws_subnet.example2.id,
+    ]
+
+    security_group_ids = [
+      aws_security_group.example1.id,
+      aws_security_group.example2.id,
+    ]
+  }
+
+  tags = {
+    Environment = "Test"
+  }
+}
+
+resource "aws_codebuild_project" "project-with-cache" {
+  name           = "test-project-cache"
+  description    = "test_codebuild_project_cache"
+  build_timeout  = 5
+  queued_timeout = 5
+
+  service_role = aws_iam_role.example.arn
+
+  artifacts {
+    type = "NO_ARTIFACTS"
+  }
+
+  cache {
+    type  = "LOCAL"
+    modes = ["LOCAL_DOCKER_LAYER_CACHE", "LOCAL_SOURCE_CACHE"]
+  }
+
+  environment {
+    compute_type                = "BUILD_GENERAL1_SMALL"
+    image                       = "aws/codebuild/amazonlinux2-x86_64-standard:4.0"
+    type                        = "LINUX_CONTAINER"
+    image_pull_credentials_type = "CODEBUILD"
+
+    environment_variable {
+      name  = "SOME_KEY1"
+      value = "SOME_VALUE1"
+    }
+  }
+
+  source {
+    type            = "GITHUB"
+    location        = "https://github.com/mitchellh/packer.git"
+    git_clone_depth = 1
+  }
+
+  tags = {
+    Environment = "Test"
+  }
+}
\ No newline at end of file
diff --git a/other_langs/terraform/codebuild/provider.tf b/other_langs/terraform/codebuild/provider.tf
new file mode 100644
index 000000000..25ef614c0
--- /dev/null
+++ b/other_langs/terraform/codebuild/provider.tf
@@ -0,0 +1,26 @@
+terraform {
+  required_providers {
+    aws = {
+      source = "hashicorp/aws"
+    }
+  }
+}
+
+provider "aws" {
+  region                      = "us-east-1"
+  s3_use_path_style           = true
+  skip_credentials_validation = true
+  skip_metadata_api_check     = true
+  skip_requesting_account_id  = true
+
+  endpoints {
+    ec2          = "http://localhost:5000"
+    codebuild    = "http://localhost:5000"
+    iam          = "http://localhost:5000"
+    logs         = "http://localhost:5000"
+    s3           = "http://localhost:5000"
+  }
+
+  access_key = "my-access-key"
+  secret_key = "my-secret-key"
+}
\ No newline at end of file
diff --git a/other_langs/terraform/ds/provider.tf b/other_langs/terraform/ds/provider.tf
new file mode 100644
index 000000000..b4101c772
--- /dev/null
+++ b/other_langs/terraform/ds/provider.tf
@@ -0,0 +1,23 @@
+terraform {
+  required_providers {
+    aws = {
+      source = "hashicorp/aws"
+    }
+  }
+}
+
+provider "aws" {
+  region                      = "us-east-1"
+  s3_use_path_style           = true
+  skip_credentials_validation = true
+  skip_metadata_api_check     = true
+  skip_requesting_account_id  = true
+
+  endpoints {
+    ec2   = "http://localhost:5000"
+    ds    = "http://localhost:5000"
+  }
+
+  access_key = "my-access-key"
+  secret_key = "my-secret-key"
+}
\ No newline at end of file
diff --git a/other_langs/terraform/ds/simple_ad.tf b/other_langs/terraform/ds/simple_ad.tf
new file mode 100644
index 000000000..3289b582c
--- /dev/null
+++ b/other_langs/terraform/ds/simple_ad.tf
@@ -0,0 +1,30 @@
+resource "aws_directory_service_directory" "bar" {
+  name     = "corp.notexample.com"
+  password = "SuperSecretPassw0rd"
+  size     = "Small"
+
+  vpc_settings {
+    vpc_id     = aws_vpc.main.id
+    subnet_ids = [aws_subnet.foo.id, aws_subnet.bar.id]
+  }
+
+  tags = {
+    Project = "foo"
+  }
+}
+
+resource "aws_vpc" "main" {
+  cidr_block = "10.0.0.0/16"
+}
+
+resource "aws_subnet" "foo" {
+  vpc_id            = aws_vpc.main.id
+  availability_zone = "us-west-2a"
+  cidr_block        = "10.0.1.0/24"
+}
+
+resource "aws_subnet" "bar" {
+  vpc_id            = aws_vpc.main.id
+  availability_zone = "us-west-2b"
+  cidr_block        = "10.0.2.0/24"
+}
\ No newline at end of file


Test patch:
diff --git a/.github/workflows/tests_terraform_examples.yml b/.github/workflows/tests_terraform_examples.yml
index 0dd188f7e..b24d748f6 100644
--- a/.github/workflows/tests_terraform_examples.yml
+++ b/.github/workflows/tests_terraform_examples.yml
@@ -11,7 +11,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        service: ["acm", "awslambda", "cloudfront", "elb", "iam", "rds", "route53", "sqs"]
+        service: ["acm", "awslambda", "cloudfront", "codebuild", "ds", "elb", "iam", "rds", "route53", "sqs"]
 
     steps:
     - uses: actions/checkout@v4
diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
index d34e6e4d5..c6c5815d1 100644
--- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
+++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
@@ -1078,7 +1078,7 @@ def test_get_template_summary_for_stack_created_by_changeset_execution():
     conn.create_change_set(
         StackName="stack_from_changeset",
         TemplateBody=json.dumps(dummy_template3),
-        ChangeSetName="test_changeset",
+        ChangeSetName="test-changeset",
         ChangeSetType="CREATE",
     )
     with pytest.raises(
@@ -1086,7 +1086,7 @@ def test_get_template_summary_for_stack_created_by_changeset_execution():
         match="GetTemplateSummary cannot be called on REVIEW_IN_PROGRESS stacks",
     ):
         conn.get_template_summary(StackName="stack_from_changeset")
-    conn.execute_change_set(ChangeSetName="test_changeset")
+    conn.execute_change_set(ChangeSetName="test-changeset")
     result = conn.get_template_summary(StackName="stack_from_changeset")
     assert result["ResourceTypes"] == ["AWS::EC2::VPC"]
     assert result["Version"] == "2010-09-09"
@@ -2555,6 +2555,46 @@ def test_create_and_update_stack_with_unknown_resource():
             )
 
 
+@mock_aws
+def test_invalid_change_set_name_starting_char():
+    cf = boto3.client("cloudformation", region_name=REGION_NAME)
+    with pytest.raises(ClientError):
+        cf.create_change_set(
+            StackName=TEST_STACK_NAME,
+            ChangeSetName="1invalid-change-set-name",
+            TemplateBody=json.dumps(dummy_template),
+            Description="Test Change Set",
+            ChangeSetType="CREATE",
+        )
+
+
+@mock_aws
+def test_invalid_change_set_name_length():
+    cf = boto3.client("cloudformation", region_name=REGION_NAME)
+    long_name = "a" * 129  # Exceeds the 128 character limit
+    with pytest.raises(ClientError):
+        cf.create_change_set(
+            StackName=TEST_STACK_NAME,
+            ChangeSetName=long_name,
+            TemplateBody=json.dumps(dummy_template),
+            Description="Test Change Set",
+            ChangeSetType="CREATE",
+        )
+
+
+@mock_aws
+def test_invalid_change_set_name_special_chars():
+    cf = boto3.client("cloudformation", region_name=REGION_NAME)
+    with pytest.raises(ClientError):
+        cf.create_change_set(
+            StackName=TEST_STACK_NAME,
+            ChangeSetName="invalid@name",
+            TemplateBody=json.dumps(dummy_template),
+            Description="Test Change Set",
+            ChangeSetType="CREATE",
+        )
+
+
 def get_role_name():
     with mock_aws():
         iam = boto3.client("iam", region_name=REGION_NAME)
diff --git a/tests/test_codebuild/test_codebuild.py b/tests/test_codebuild/test_codebuild.py
index 057218eb1..a2129de35 100644
--- a/tests/test_codebuild/test_codebuild.py
+++ b/tests/test_codebuild/test_codebuild.py
@@ -37,6 +37,7 @@ def test_codebuild_create_project_s3_artifacts():
         artifacts=artifacts,
         environment=environment,
         serviceRole=service_role,
+        tags=[{"key": "k1", "value": "v1"}],
     )["project"]
 
     assert "serviceRole" in project
@@ -51,6 +52,8 @@ def test_codebuild_create_project_s3_artifacts():
     assert project["source"] == {"location": "bucketname/path/file.zip", "type": "S3"}
     assert project["artifacts"] == {"location": "bucketname", "type": "S3"}
 
+    assert project["tags"] == [{"key": "k1", "value": "v1"}]
+
 
 @mock_aws
 def test_codebuild_create_project_no_artifacts():
@@ -612,3 +615,42 @@ def test_codebuild_stop_build_bad_uid():
     with pytest.raises(client.exceptions.InvalidInputException) as err:
         client.stop_build(id=f"some_project{uuid1()}")
     assert err.value.response["Error"]["Code"] == "InvalidInputException"
+
+
+@mock_aws
+def test_codebuild_batch_get_projects():
+    client = boto3.client("codebuild", region_name="eu-central-1")
+
+    name = "some_project"
+    source = dict()
+    source["type"] = "S3"
+    # repository location for S3
+    source["location"] = "bucketname/path/file.zip"
+    artifacts = {"type": "NO_ARTIFACTS"}
+
+    environment = dict()
+    environment["type"] = "LINUX_CONTAINER"
+    environment["image"] = "contents_not_validated"
+    environment["computeType"] = "BUILD_GENERAL1_SMALL"
+    service_role = (
+        f"arn:aws:iam::{ACCOUNT_ID}:role/service-role/my-codebuild-service-role"
+    )
+
+    project_arn = client.create_project(
+        name=name,
+        source=source,
+        artifacts=artifacts,
+        environment=environment,
+        serviceRole=service_role,
+    )["project"]["arn"]
+
+    projects_batch = client.batch_get_projects(names=[name])["projects"]
+    assert len(projects_batch) == 1
+    assert projects_batch[0]["name"] == name
+    assert projects_batch[0]["arn"] == project_arn
+
+    # Assert we can also use the ARN
+    projects_batch = client.batch_get_projects(names=[project_arn])["projects"]
+    assert len(projects_batch) == 1
+    assert projects_batch[0]["name"] == name
+    assert projects_batch[0]["arn"] == project_arn
diff --git a/tests/test_dynamodb/test_dynamodb_update_expressions.py b/tests/test_dynamodb/test_dynamodb_update_expressions.py
index 5ffcedbac..526b00fce 100644
--- a/tests/test_dynamodb/test_dynamodb_update_expressions.py
+++ b/tests/test_dynamodb/test_dynamodb_update_expressions.py
@@ -187,3 +187,22 @@ def test_update_item_add_empty_set(table_name=None):
     assert dynamodb.scan(TableName=table_name)["Items"] == [
         {"pk": {"S": "foo"}, "stringset": {"SS": ["item1"]}}
     ]
+
+
+@pytest.mark.aws_verified
+@dynamodb_aws_verified()
+def test_update_item_with_empty_values(table_name=None):
+    dynamodb = boto3.client("dynamodb", "us-east-1")
+
+    dynamodb.put_item(TableName=table_name, Item={"pk": {"S": "foo"}})
+    with pytest.raises(ClientError) as exc:
+        dynamodb.update_item(
+            TableName=table_name,
+            Key={"pk": {"S": "foo"}},
+            UpdateExpression="SET #d = :s",
+            ExpressionAttributeNames={"#d": "d"},
+            ExpressionAttributeValues={},
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationException"
+    assert err["Message"] == "ExpressionAttributeValues must not be empty"
diff --git a/tests/test_ec2/test_security_groups.py b/tests/test_ec2/test_security_groups.py
index 73a371e1b..519b5c2c6 100644
--- a/tests/test_ec2/test_security_groups.py
+++ b/tests/test_ec2/test_security_groups.py
@@ -12,6 +12,7 @@ from botocore.exceptions import ClientError
 from moto import mock_aws, settings
 from moto.core import DEFAULT_ACCOUNT_ID
 from moto.ec2 import ec2_backends
+from tests import aws_verified
 
 REGION = "us-east-1"
 
@@ -132,17 +133,30 @@ def test_create_two_security_groups_with_same_name_in_different_vpc():
     assert name in group_names
 
 
-@mock_aws
+@aws_verified
+@pytest.mark.aws_verified
 def test_create_two_security_groups_in_vpc_with_ipv6_enabled():
     ec2 = boto3.resource("ec2", region_name=REGION)
     vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16", AmazonProvidedIpv6CidrBlock=True)
+    security_group = None
 
-    security_group = ec2.create_security_group(
-        GroupName=str(uuid4()), Description="Test security group sg01", VpcId=vpc.id
-    )
+    try:
+        security_group = ec2.create_security_group(
+            GroupName=str(uuid4()), Description="Test", VpcId=vpc.id
+        )
 
-    # The security group must have two defaul egress rules (one for ipv4 and aonther for ipv6)
-    assert len(security_group.ip_permissions_egress) == 2
+        # The security group must have one egress rule with an IP range for ipv4 and ipv6
+        assert len(security_group.ip_permissions_egress) == 1
+        assert security_group.ip_permissions_egress[0]["IpRanges"] == [
+            {"CidrIp": "0.0.0.0/0"}
+        ]
+        assert security_group.ip_permissions_egress[0]["Ipv6Ranges"] == [
+            {"CidrIpv6": "::/0"}
+        ]
+    finally:
+        if security_group:
+            security_group.delete()
+        vpc.delete()
 
 
 @mock_aws
@@ -868,7 +882,6 @@ def test_description_in_ip_permissions():
     assert result["SecurityGroupRules"][1]["FromPort"] == 27017
     assert result["SecurityGroupRules"][1]["ToPort"] == 27018
 
-    assert result["SecurityGroupRules"][2]["Description"] == ""
     assert result["SecurityGroupRules"][2]["IsEgress"] is True
     assert result["SecurityGroupRules"][2]["FromPort"] == -1
     assert result["SecurityGroupRules"][2]["ToPort"] == -1
@@ -2025,3 +2038,284 @@ def test_invalid_security_group_id_in_rules_search():
         Filters=[{"Name": "group-id", "Values": [group_id]}]
     )
     assert len(response["SecurityGroupRules"]) == 1
+
+
+@aws_verified
+@pytest.mark.aws_verified
+@pytest.mark.parametrize("is_ingress", [True, False], ids=["ingress", "egress"])
+def test_authorize_security_group_rules_with_different_ipranges_or_prefixes(is_ingress):
+    is_egress = not is_ingress
+    sts = boto3.client("sts", "us-east-1")
+    account_id = sts.get_caller_identity()["Account"]
+
+    ec2_client = boto3.client("ec2", "us-east-1")
+    vpc = ec2_client.create_vpc(CidrBlock="10.0.0.0/24")
+    vpc_id = vpc["Vpc"]["VpcId"]
+    sg_id = pl_id = None
+
+    try:
+        sg = ec2_client.create_security_group(
+            Description="test", GroupName=f"test_{str(uuid4())[0:6]}", VpcId=vpc_id
+        )
+        sg_id = sg["GroupId"]
+
+        prefix_list = ec2_client.create_managed_prefix_list(
+            PrefixListName=f"test{str(uuid4())[0:6]}",
+            AddressFamily="IPv4",
+            MaxEntries=1,
+        )
+        pl_id = prefix_list["PrefixList"]["PrefixListId"]
+
+        rules = [
+            # starting rule with tcp/22
+            (22, "tcp", {"IpRanges": [{"CidrIp": "10.0.0.0/24"}]}),
+            # keeping the port/protocol the same but changing the CIDR will NOT show up in describe_security_group_rules
+            (22, "tcp", {"IpRanges": [{"CidrIp": "10.0.1.0/24"}]}),
+            # changing just the port will show up in describe_security_group_rules
+            (80, "tcp", {"IpRanges": [{"CidrIp": "10.0.0.0/24"}]}),
+            # changing just the protocol will show up in describe_security_group_rules
+            (22, "udp", {"IpRanges": [{"CidrIp": "10.0.0.0/24"}]}),
+            # changing the port and protocol will show up in describe_security_group_rules
+            (-1, "-1", {"IpRanges": [{"CidrIp": "10.0.0.0/24"}]}),
+            #
+            # The same issues are present with IPv6 in that, with matching port/protocol to the original rule, it will be omitted from describe_security_group_rules
+            (22, "tcp", {"Ipv6Ranges": [{"CidrIpv6": "2001:db8::/32"}]}),
+            # changing the port to a new port will cause it to show up in describe_security_group_rules
+            # however, a the value cannot be seen. An empty `CidrIpv4` key is returned and the `CidrIpv6` key is not returned.
+            (443, "tcp", {"Ipv6Ranges": [{"CidrIpv6": "2001:db8::/64"}]}),
+            #
+            # The same issues are present with PrefixListIds in that, with matching port/protocol to the original rule, it will be omitted from describe_security_group_rules
+            (22, "tcp", {"PrefixListIds": [{"PrefixListId": pl_id}]}),
+            # changing the port to a new port will cause it to show up in describe_security_group_rules
+            # however there is no indication that the rule is for a prefix list.
+            (8080, "tcp", {"PrefixListIds": [{"PrefixListId": pl_id}]}),
+            #
+            # the same issues are present with security group references in that, with matching port/protocol to the original rule, it will be omitted from describe_security_group_rules
+            (22, "tcp", {"UserIdGroupPairs": [{"GroupId": sg_id}]}),
+            # changing the port to a new port will cause it to show up in describe_security_group_rules
+            # however there is no indication of this pointing to itself as a target.
+            (25, "tcp", {"UserIdGroupPairs": [{"GroupId": sg_id}]}),
+        ]
+        method = (
+            ec2_client.authorize_security_group_ingress
+            if is_ingress
+            else ec2_client.authorize_security_group_egress
+        )
+        for rule in rules:
+            method(
+                GroupId=sg_id,
+                IpPermissions=[
+                    {
+                        "FromPort": rule[0],
+                        "IpProtocol": rule[1],
+                        "ToPort": rule[0],
+                        **rule[2],
+                    }
+                ],
+            )
+        created_rules = ec2_client.describe_security_group_rules(
+            Filters=[{"Name": "group-id", "Values": [sg_id]}]
+        )["SecurityGroupRules"]
+        for rule in created_rules:
+            rule.pop("GroupOwnerId", None)
+            rule.pop("Tags", None)
+            rule.pop("SecurityGroupRuleId", None)
+            del rule["GroupId"]
+
+        assert {
+            "CidrIpv6": "2001:db8::/64",
+            "FromPort": 443,
+            "IpProtocol": "tcp",
+            "IsEgress": is_egress,
+            "ToPort": 443,
+        } in created_rules
+        assert {
+            "FromPort": 22,
+            "IpProtocol": "tcp",
+            "IsEgress": is_egress,
+            "ReferencedGroupInfo": {"GroupId": sg_id, "UserId": account_id},
+            "ToPort": 22,
+        } in created_rules
+        assert {
+            "FromPort": 25,
+            "IpProtocol": "tcp",
+            "IsEgress": is_egress,
+            "ReferencedGroupInfo": {"GroupId": sg_id, "UserId": account_id},
+            "ToPort": 25,
+        } in created_rules
+        assert {
+            "CidrIpv4": "10.0.0.0/24",
+            "FromPort": 22,
+            "IpProtocol": "tcp",
+            "IsEgress": is_egress,
+            "ToPort": 22,
+        } in created_rules
+        assert {
+            "CidrIpv4": "10.0.0.0/24",
+            "FromPort": -1,
+            "IpProtocol": "-1",
+            "IsEgress": is_egress,
+            "ToPort": -1,
+        } in created_rules
+        assert {
+            "FromPort": 8080,
+            "IpProtocol": "tcp",
+            "IsEgress": is_egress,
+            "PrefixListId": pl_id,
+            "ToPort": 8080,
+        } in created_rules
+        assert {
+            "CidrIpv6": "2001:db8::/32",
+            "FromPort": 22,
+            "IpProtocol": "tcp",
+            "IsEgress": is_egress,
+            "ToPort": 22,
+        } in created_rules
+        assert {
+            "FromPort": 22,
+            "IpProtocol": "tcp",
+            "IsEgress": is_egress,
+            "PrefixListId": pl_id,
+            "ToPort": 22,
+        } in created_rules
+        assert {
+            "CidrIpv4": "10.0.0.0/24",
+            "FromPort": 80,
+            "IpProtocol": "tcp",
+            "IsEgress": is_egress,
+            "ToPort": 80,
+        } in created_rules
+        assert {
+            "CidrIpv4": "10.0.1.0/24",
+            "FromPort": 22,
+            "IpProtocol": "tcp",
+            "IsEgress": is_egress,
+            "ToPort": 22,
+        } in created_rules
+        assert {
+            "CidrIpv4": "0.0.0.0/0",
+            "FromPort": -1,
+            "IpProtocol": "-1",
+            "IsEgress": True,
+            "ToPort": -1,
+        } in created_rules
+        assert {
+            "CidrIpv4": "10.0.0.0/24",
+            "FromPort": 22,
+            "IpProtocol": "udp",
+            "IsEgress": is_egress,
+            "ToPort": 22,
+        } in created_rules
+
+        groups = ec2_client.describe_security_groups(GroupIds=[sg_id])[
+            "SecurityGroups"
+        ][0]
+        permissions = groups["IpPermissions"]
+        permissions_egress = groups["IpPermissionsEgress"]
+        # Drop empty fields
+        permissions = [{k: p[k] for k in p if p[k]} for p in permissions]
+        permissions_egress = [{k: p[k] for k in p if p[k]} for p in permissions_egress]
+
+        if is_ingress:
+            assert len(permissions) == 7
+            assert {
+                "FromPort": 80,
+                "IpProtocol": "tcp",
+                "IpRanges": [{"CidrIp": "10.0.0.0/24"}],
+                "ToPort": 80,
+            } in permissions
+            assert {
+                "FromPort": 8080,
+                "IpProtocol": "tcp",
+                "PrefixListIds": [{"PrefixListId": pl_id}],
+                "ToPort": 8080,
+            } in permissions
+            assert {
+                "IpProtocol": "-1",
+                "IpRanges": [{"CidrIp": "10.0.0.0/24"}],
+            } in permissions
+            assert {
+                "FromPort": 22,
+                "IpProtocol": "udp",
+                "IpRanges": [{"CidrIp": "10.0.0.0/24"}],
+                "ToPort": 22,
+            } in permissions
+            assert {
+                "FromPort": 22,
+                "IpProtocol": "tcp",
+                "IpRanges": [{"CidrIp": "10.0.0.0/24"}, {"CidrIp": "10.0.1.0/24"}],
+                "Ipv6Ranges": [{"CidrIpv6": "2001:db8::/32"}],
+                "PrefixListIds": [{"PrefixListId": pl_id}],
+                "ToPort": 22,
+                "UserIdGroupPairs": [{"GroupId": sg_id, "UserId": account_id}],
+            } in permissions
+            assert {
+                "FromPort": 25,
+                "IpProtocol": "tcp",
+                "ToPort": 25,
+                "UserIdGroupPairs": [{"GroupId": sg_id, "UserId": account_id}],
+            } in permissions
+            assert {
+                "FromPort": 443,
+                "IpProtocol": "tcp",
+                "Ipv6Ranges": [{"CidrIpv6": "2001:db8::/64"}],
+                "ToPort": 443,
+            } in permissions
+
+            assert permissions_egress == [
+                {"IpProtocol": "-1", "IpRanges": [{"CidrIp": "0.0.0.0/0"}]}
+            ]
+        if is_egress:
+            assert len(permissions) == 0
+            assert len(permissions_egress) == 7
+
+            assert {
+                "FromPort": 80,
+                "IpProtocol": "tcp",
+                "IpRanges": [{"CidrIp": "10.0.0.0/24"}],
+                "ToPort": 80,
+            } in permissions_egress
+            assert {
+                "FromPort": 8080,
+                "IpProtocol": "tcp",
+                "PrefixListIds": [{"PrefixListId": pl_id}],
+                "ToPort": 8080,
+            } in permissions_egress
+            assert {
+                "IpProtocol": "-1",
+                "IpRanges": [{"CidrIp": "0.0.0.0/0"}, {"CidrIp": "10.0.0.0/24"}],
+            } in permissions_egress
+            assert {
+                "FromPort": 22,
+                "IpProtocol": "udp",
+                "IpRanges": [{"CidrIp": "10.0.0.0/24"}],
+                "ToPort": 22,
+            } in permissions_egress
+            assert {
+                "FromPort": 22,
+                "IpProtocol": "tcp",
+                "IpRanges": [{"CidrIp": "10.0.0.0/24"}, {"CidrIp": "10.0.1.0/24"}],
+                "Ipv6Ranges": [{"CidrIpv6": "2001:db8::/32"}],
+                "PrefixListIds": [{"PrefixListId": pl_id}],
+                "ToPort": 22,
+                "UserIdGroupPairs": [{"GroupId": sg_id, "UserId": account_id}],
+            } in permissions_egress
+            assert {
+                "FromPort": 25,
+                "IpProtocol": "tcp",
+                "ToPort": 25,
+                "UserIdGroupPairs": [{"GroupId": sg_id, "UserId": account_id}],
+            } in permissions_egress
+            assert {
+                "FromPort": 443,
+                "IpProtocol": "tcp",
+                "Ipv6Ranges": [{"CidrIpv6": "2001:db8::/64"}],
+                "ToPort": 443,
+            } in permissions_egress
+
+    finally:
+        if pl_id:
+            ec2_client.delete_managed_prefix_list(PrefixListId=pl_id)
+        if sg_id:
+            ec2_client.delete_security_group(GroupId=sg_id)
+        ec2_client.delete_vpc(VpcId=vpc_id)
diff --git a/tests/test_kms/test_model.py b/tests/test_kms/test_model.py
index 523b1d748..f6532ff65 100644
--- a/tests/test_kms/test_model.py
+++ b/tests/test_kms/test_model.py
@@ -33,7 +33,7 @@ def test_encrypt_key_arn(backend, key):
 
 
 def test_encrypt_alias_name(backend, key):
-    backend.add_alias(key.id, "alias/test/test")
+    backend.create_alias(key.id, "alias/test/test")
 
     ciphertext, arn = backend.encrypt("alias/test/test", PLAINTEXT, {})
 
@@ -42,7 +42,7 @@ def test_encrypt_alias_name(backend, key):
 
 
 def test_encrypt_alias_arn(backend, key):
-    backend.add_alias(key.id, "alias/test/test")
+    backend.create_alias(key.id, "alias/test/test")
 
     ciphertext, arn = backend.encrypt(
         f"arn:aws:kms:{REGION}:{key.account_id}:alias/test/test", PLAINTEXT, {}
diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py
index 9fa3d567d..5c1ee7d18 100644
--- a/tests/test_organizations/test_organizations_boto3.py
+++ b/tests/test_organizations/test_organizations_boto3.py
@@ -1,12 +1,14 @@
 import json
+import os
 import re
 from datetime import datetime
+from unittest import SkipTest, mock
 
 import boto3
 import pytest
 from botocore.exceptions import ClientError
 
-from moto import mock_aws
+from moto import mock_aws, settings
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
 from moto.organizations import utils
 from moto.organizations.exceptions import InvalidInputException, TargetNotFoundException
@@ -75,6 +77,39 @@ def test_create_organization_without_feature_set():
     assert response["Organization"]["FeatureSet"] == "ALL"
 
 
+@mock_aws
+def test_create_account_creates_role():
+    if not settings.TEST_DECORATOR_MODE:
+        raise SkipTest("Involves changing account using env variable")
+    orgs_client = boto3.client("organizations", region_name="us-west-2")
+    orgs_client.create_organization()
+    account = orgs_client.create_account(AccountName="test", Email="test@test.xyz")
+    account_id = account["CreateAccountStatus"]["AccountId"]
+
+    with mock.patch.dict(os.environ, {"MOTO_ACCOUNT_ID": account_id}):
+        iam_client = boto3.client("iam", "us-east-1")
+        iam_client.get_role(RoleName="OrganizationAccountAccessRole")
+
+
+@mock_aws
+def test_create_account_creates_custom_role():
+    if not settings.TEST_DECORATOR_MODE:
+        raise SkipTest("Involves changing account using env variable")
+
+    orgs_client = boto3.client("organizations", region_name="us-west-2")
+    _ = orgs_client.create_organization()
+    account = orgs_client.create_account(
+        AccountName="test_account",
+        Email="test@test.xyz",
+        RoleName="CustomOrganizationRole",
+    )
+    account_id = account["CreateAccountStatus"]["AccountId"]
+
+    with mock.patch.dict(os.environ, {"MOTO_ACCOUNT_ID": account_id}):
+        iam_client = boto3.client("iam", "us-east-1")
+        iam_client.get_role(RoleName="CustomOrganizationRole")
+
+
 @mock_aws
 def test_describe_organization():
     client = boto3.client("organizations", region_name="us-east-1")