Repository: getmoto/moto
Pull request #6812 (2023-09-14 14:55:56)

ELBv2: fix parity in describe_target_group
# Motivation
This PR fixes minor parity gaps with aws in `describe_target_groups` api

# Fixed
- sorting the list of target group before returning it
- multiple arguments in describe call raises an exception

# Added
- Added test cases for `describe_target_groups` api


Patch:
diff --git a/moto/cognitoidp/models.py b/moto/cognitoidp/models.py
index fcc943238..2cccdec56 100644
--- a/moto/cognitoidp/models.py
+++ b/moto/cognitoidp/models.py
@@ -382,7 +382,7 @@ DEFAULT_USER_POOL_CONFIG: Dict[str, Any] = {
 
 class CognitoIdpUserPool(BaseModel):
 
-    MAX_ID_LENGTH = 56
+    MAX_ID_LENGTH = 55
 
     def __init__(
         self, account_id: str, region: str, name: str, extended_config: Dict[str, Any]
diff --git a/moto/ebs/responses.py b/moto/ebs/responses.py
index 75d1549f9..0e9b2ad99 100644
--- a/moto/ebs/responses.py
+++ b/moto/ebs/responses.py
@@ -24,7 +24,7 @@ class EBSResponse(BaseResponse):
             return self.start_snapshot()
 
     def snapshot_block(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE:  # type: ignore[return]
-        self.setup_class(request, full_url, headers)
+        self.setup_class(request, full_url, headers, use_raw_body=True)
         if request.method == "PUT":
             return self.put_snapshot_block(full_url, headers)
         if request.method == "GET":
@@ -59,7 +59,7 @@ class EBSResponse(BaseResponse):
         self.setup_class(request, full_url, headers)
         snapshot_id = full_url.split("/")[-1]
         status = self.ebs_backend.complete_snapshot(snapshot_id=snapshot_id)
-        return 200, {}, json.dumps(status)
+        return 202, {}, json.dumps(status)
 
     def put_snapshot_block(self, full_url: str, headers: Any) -> TYPE_RESPONSE:
         """
@@ -82,7 +82,7 @@ class EBSResponse(BaseResponse):
             data_length=data_length,
         )
         return (
-            200,
+            201,
             {
                 "x-amz-Checksum": checksum,
                 "x-amz-Checksum-Algorithm": checksum_algorithm,
diff --git a/moto/elbv2/exceptions.py b/moto/elbv2/exceptions.py
index 9550fcb9e..dd2fa6ee9 100644
--- a/moto/elbv2/exceptions.py
+++ b/moto/elbv2/exceptions.py
@@ -35,9 +35,7 @@ class SubnetNotFoundError(ELBClientError):
 
 class TargetGroupNotFoundError(ELBClientError):
     def __init__(self) -> None:
-        super().__init__(
-            "TargetGroupNotFound", "The specified target group does not exist."
-        )
+        super().__init__("TargetGroupNotFound", "One or more target groups not found")
 
 
 class TooManyTagsError(ELBClientError):
diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py
index 0a45bd9e9..2ae4f3f51 100644
--- a/moto/elbv2/models.py
+++ b/moto/elbv2/models.py
@@ -822,6 +822,13 @@ class ELBv2Backend(BaseBackend):
                 raise PriorityInUseError()
 
         self._validate_actions(fake_actions)
+        for action in fake_actions:
+            if action.type == "forward":
+                found_arns = self._get_target_group_arns_from(action_data=action.data)
+                for arn in found_arns:
+                    target_group = self.target_groups[arn]
+                    target_group.load_balancer_arns.append(listener.load_balancer_arn)
+
         arn = listener_arn.replace(":listener/", ":listener-rule/")
         arn += f"/{mock_random.get_random_hex(16)}"
 
@@ -1370,20 +1377,33 @@ Member must satisfy regular expression pattern: {expression}"
         target_group_arns: List[str],
         names: Optional[List[str]],
     ) -> Iterable[FakeTargetGroup]:
+
+        args = sum(bool(arg) for arg in [load_balancer_arn, target_group_arns, names])
+
+        if args > 1:
+            raise ValidationError(
+                "Target group names, target group ARNs, and a load balancer ARN cannot be specified at the same time"
+            )
+
         if load_balancer_arn:
             if load_balancer_arn not in self.load_balancers:
                 raise LoadBalancerNotFoundError()
-            return [
+            target_groups = [
                 tg
                 for tg in self.target_groups.values()
                 if load_balancer_arn in tg.load_balancer_arns
             ]
+            if target_groups is None or len(target_groups) == 0:
+                raise TargetGroupNotFoundError()
+            return sorted(target_groups, key=lambda tg: tg.name)
 
         if target_group_arns:
             try:
-                return [self.target_groups[arn] for arn in target_group_arns]
+                target_groups = [self.target_groups[arn] for arn in target_group_arns]
+                return sorted(target_groups, key=lambda tg: tg.name)
             except KeyError:
                 raise TargetGroupNotFoundError()
+
         if names:
             matched = []
             for name in names:
@@ -1394,9 +1414,9 @@ Member must satisfy regular expression pattern: {expression}"
                 if not found:
                     raise TargetGroupNotFoundError()
                 matched.append(found)
-            return matched
+            return sorted(matched, key=lambda tg: tg.name)
 
-        return self.target_groups.values()
+        return sorted(self.target_groups.values(), key=lambda tg: tg.name)
 
     def describe_listeners(
         self, load_balancer_arn: Optional[str], listener_arns: List[str]
diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py
index 652d5b9b9..443d92fbb 100644
--- a/moto/elbv2/responses.py
+++ b/moto/elbv2/responses.py
@@ -1070,6 +1070,7 @@ DESCRIBE_TARGET_GROUPS_TEMPLATE = """<DescribeTargetGroupsResponse xmlns="http:/
         {% if target_group.vpc_id %}
         <VpcId>{{ target_group.vpc_id }}</VpcId>
         {% endif %}
+        <IpAddressType>{{ target_group.ip_address_type }}</IpAddressType>
         <HealthCheckProtocol>{{ target_group.healthcheck_protocol }}</HealthCheckProtocol>
         {% if target_group.healthcheck_port %}<HealthCheckPort>{{ target_group.healthcheck_port }}</HealthCheckPort>{% endif %}
         <HealthCheckPath>{{ target_group.healthcheck_path or '' }}</HealthCheckPath>


Test patch:
diff --git a/.github/workflows/tests_sdk_dotnet.yml b/.github/workflows/tests_sdk_dotnet.yml
index b82fe65d1..4bf92e76f 100644
--- a/.github/workflows/tests_sdk_dotnet.yml
+++ b/.github/workflows/tests_sdk_dotnet.yml
@@ -28,8 +28,8 @@ jobs:
         restore-keys: |
           ${{ runner.os }}-nuget
     - name: Install dependencies
-      run: cd other_langs/tests_dotnet && dotnet restore ExampleTestProject/
+      run: cd other_langs/tests_dotnet && dotnet restore ExampleTestProject/ && dotnet restore ebs
     - name: Run tests
       run: |
         mkdir ~/.aws && touch ~/.aws/credentials && echo -e "[default]\naws_access_key_id = test\naws_secret_access_key = test" > ~/.aws/credentials
-        cd other_langs/tests_dotnet && dotnet test ExampleTestProject/
+        cd other_langs/tests_dotnet && dotnet test ExampleTestProject/ && dotnet restore ebs
diff --git a/other_langs/tests_dotnet/ebs/ElasticBlockStore.csproj b/other_langs/tests_dotnet/ebs/ElasticBlockStore.csproj
new file mode 100644
index 000000000..a58828d3f
--- /dev/null
+++ b/other_langs/tests_dotnet/ebs/ElasticBlockStore.csproj
@@ -0,0 +1,26 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net7.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+
+	  <IsPackable>true</IsPackable>
+	  <PackAsTool>true</PackAsTool>
+
+    <UserSecretsId>45a910a7-aba2-43ee-ad3c-ccad57f044d9</UserSecretsId>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="AWSSDK.EBS" Version="3.7.100.153" />
+    <PackageReference Include="FluentAssertions" Version="6.8.0" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
+    <PackageReference Include="xunit" Version="2.4.1" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+  </ItemGroup>
+
+</Project>
diff --git a/other_langs/tests_dotnet/ebs/UnitTest.cs b/other_langs/tests_dotnet/ebs/UnitTest.cs
new file mode 100644
index 000000000..50d8ce402
--- /dev/null
+++ b/other_langs/tests_dotnet/ebs/UnitTest.cs
@@ -0,0 +1,56 @@
+using FluentAssertions;
+
+// To interact with Amazon EBS
+using Amazon.EBS;
+using Amazon.EBS.Model;
+
+using System.Security.Cryptography;
+
+
+public class EbsSnashotTest
+{
+    [Fact]
+    public async Task TestSnapshotCreation()
+    {
+
+        AmazonEBSConfig config = new AmazonEBSConfig()
+        {
+            ServiceURL = "http://localhost:5000",
+        };
+
+        AmazonEBSClient ebsClient = new AmazonEBSClient(config);
+
+        // https://docs.aws.amazon.com/ebs/latest/APIReference/API_StartSnapshot.html
+        var startSnapshotRequest = new StartSnapshotRequest { VolumeSize = 1 };
+        var startSnapshotResponse = await ebsClient.StartSnapshotAsync(startSnapshotRequest);
+        startSnapshotResponse.Status.Should().Be("pending");    
+
+        // https://docs.aws.amazon.com/ebs/latest/APIReference/API_PutSnapshotBlock.html
+        var blockData = new byte[] { 0x01, 0x02, 0x03, 0x04 };
+        Stream blockDataStream = new MemoryStream(blockData);
+        SHA256Managed sha256hasher = new SHA256Managed();
+        byte[] sha256Hash = sha256hasher.ComputeHash(blockDataStream);
+        var sha256checksum = Convert.ToBase64String(sha256Hash);
+        var putSnapshotBlockRequest = new PutSnapshotBlockRequest
+        {
+            BlockIndex = 0,
+            Checksum = sha256checksum,
+            ChecksumAlgorithm = "SHA256",
+            DataLength = blockData.Length,
+            SnapshotId = startSnapshotResponse.SnapshotId,
+            BlockData = blockDataStream
+        };
+        var putSnapshotBlockResponse = await ebsClient.PutSnapshotBlockAsync(putSnapshotBlockRequest);
+        putSnapshotBlockResponse.Checksum.Should().Be(sha256checksum);
+        putSnapshotBlockResponse.ChecksumAlgorithm.Should().Be("SHA256");
+
+        https://docs.aws.amazon.com/ebs/latest/APIReference/API_CompleteSnapshot.html
+        var completeSnapshotRequest = new CompleteSnapshotRequest
+        {
+            ChangedBlocksCount = 1,
+            SnapshotId = startSnapshotResponse.SnapshotId
+        };
+        var completeSnapshotResponse = await ebsClient.CompleteSnapshotAsync(completeSnapshotRequest);
+        completeSnapshotResponse.Status.Should().Be("completed");
+    }
+}
diff --git a/other_langs/tests_dotnet/ebs/Usings.cs b/other_langs/tests_dotnet/ebs/Usings.cs
new file mode 100644
index 000000000..8c927eb74
--- /dev/null
+++ b/other_langs/tests_dotnet/ebs/Usings.cs
@@ -0,0 +1,1 @@
+global using Xunit;
\ No newline at end of file
diff --git a/tests/test_ebs/test_ebs.py b/tests/test_ebs/test_ebs.py
index 561811a77..06cc49dfc 100644
--- a/tests/test_ebs/test_ebs.py
+++ b/tests/test_ebs/test_ebs.py
@@ -51,7 +51,7 @@ def test_complete_snapshot():
 
 @mock_ebs
 def test_put_snapshot_block():
-    data = b"data for this specific block"
+    data = b"data for this specific block\xbf"
     checksum = hashlib.sha256(data).hexdigest()
     client = boto3.client("ebs", region_name="eu-west-1")
     snapshot_id = client.start_snapshot(VolumeSize=720)["SnapshotId"]
diff --git a/tests/test_elbv2/test_elbv2_listener_rules.py b/tests/test_elbv2/test_elbv2_listener_rules.py
index 56555d1f0..6c2b7a224 100644
--- a/tests/test_elbv2/test_elbv2_listener_rules.py
+++ b/tests/test_elbv2/test_elbv2_listener_rules.py
@@ -518,3 +518,10 @@ def test_create_rule_action_forward_target_group():
     assert rule["Priority"] == "99"
     assert rule["Conditions"] == []
     assert rule["Actions"][0] == action
+
+    # assert describe_target_group by loadbalancer_arn response
+    load_balancer_arn = conn.describe_listeners(ListenerArns=[http_listener_arn])[
+        "Listeners"
+    ][0]["LoadBalancerArn"]
+    response = conn.describe_target_groups(LoadBalancerArn=load_balancer_arn)
+    assert len(response.get("TargetGroups")) == 1
diff --git a/tests/test_elbv2/test_elbv2_target_groups.py b/tests/test_elbv2/test_elbv2_target_groups.py
index 53e311e11..1370cb8fe 100644
--- a/tests/test_elbv2/test_elbv2_target_groups.py
+++ b/tests/test_elbv2/test_elbv2_target_groups.py
@@ -122,7 +122,7 @@ def test_create_target_group_and_listeners():
     http_listener_arn = listener["ListenerArn"]
 
     response = conn.describe_target_groups(
-        LoadBalancerArn=load_balancer_arn, Names=["a-target"]
+        LoadBalancerArn=load_balancer_arn,
     )
     assert len(response["TargetGroups"]) == 1
 
@@ -455,17 +455,23 @@ def test_describe_invalid_target_group():
         conn.describe_target_groups(Names=["invalid"])
     err = exc.value.response["Error"]
     assert err["Code"] == "TargetGroupNotFound"
-    assert err["Message"] == "The specified target group does not exist."
+    assert err["Message"] == "One or more target groups not found"
 
 
 @mock_elbv2
 @mock_ec2
-def test_describe_target_groups_no_arguments():
+def test_describe_target_groups():
+    elbv2 = boto3.client("elbv2", region_name="us-east-1")
+
     response, vpc, _, _, _, conn = create_load_balancer()
 
+    lb_arn = response["LoadBalancers"][0]["LoadBalancerArn"]
     assert "LoadBalancerArn" in response["LoadBalancers"][0]
 
-    conn.create_target_group(
+    groups = conn.describe_target_groups()["TargetGroups"]
+    assert len(groups) == 0
+
+    response = conn.create_target_group(
         Name="a-target",
         Protocol="HTTP",
         Port=8080,
@@ -479,11 +485,84 @@ def test_describe_target_groups_no_arguments():
         UnhealthyThresholdCount=2,
         Matcher={"HttpCode": "201"},
     )
+    arn_a = response["TargetGroups"][0]["TargetGroupArn"]
+
+    conn.create_listener(
+        LoadBalancerArn=lb_arn,
+        Protocol="HTTP",
+        Port=80,
+        DefaultActions=[{"Type": "forward", "TargetGroupArn": arn_a}],
+    )
 
     groups = conn.describe_target_groups()["TargetGroups"]
     assert len(groups) == 1
     assert groups[0]["Matcher"] == {"HttpCode": "201"}
 
+    response = elbv2.create_target_group(
+        Name="c-target",
+        Protocol="HTTP",
+        Port=8081,
+        VpcId=vpc.id,
+    )
+    arn_c = response["TargetGroups"][0]["TargetGroupArn"]
+    groups = conn.describe_target_groups()["TargetGroups"]
+    assert len(groups) == 2
+    assert groups[0]["TargetGroupName"] == "a-target"
+    assert groups[1]["TargetGroupName"] == "c-target"
+
+    response = elbv2.create_target_group(
+        Name="b-target",
+        Protocol="HTTP",
+        Port=8082,
+        VpcId=vpc.id,
+    )
+    arn_b = response["TargetGroups"][0]["TargetGroupArn"]
+    groups = conn.describe_target_groups()["TargetGroups"]
+    assert len(groups) == 3
+    assert groups[0]["TargetGroupName"] == "a-target"
+    assert groups[1]["TargetGroupName"] == "b-target"
+    assert groups[2]["TargetGroupName"] == "c-target"
+
+    groups = conn.describe_target_groups(Names=["a-target"])["TargetGroups"]
+    assert len(groups) == 1
+    assert groups[0]["TargetGroupName"] == "a-target"
+
+    groups = conn.describe_target_groups(Names=["a-target", "b-target"])["TargetGroups"]
+    assert len(groups) == 2
+    assert groups[0]["TargetGroupName"] == "a-target"
+    assert groups[1]["TargetGroupName"] == "b-target"
+
+    groups = conn.describe_target_groups(TargetGroupArns=[arn_b])["TargetGroups"]
+    assert len(groups) == 1
+    assert groups[0]["TargetGroupName"] == "b-target"
+
+    groups = conn.describe_target_groups(TargetGroupArns=[arn_b, arn_c])["TargetGroups"]
+    assert len(groups) == 2
+    assert groups[0]["TargetGroupName"] == "b-target"
+    assert groups[1]["TargetGroupName"] == "c-target"
+
+    groups = conn.describe_target_groups(LoadBalancerArn=lb_arn)["TargetGroups"]
+    assert len(groups) == 1
+    assert groups[0]["TargetGroupName"] == "a-target"
+
+    response = conn.create_target_group(
+        Name="d-target",
+        Protocol="HTTP",
+        Port=8082,
+        VpcId=vpc.id,
+    )
+    arn_d = response["TargetGroups"][0]["TargetGroupArn"]
+    conn.create_listener(
+        LoadBalancerArn=lb_arn,
+        Protocol="HTTP",
+        Port=80,
+        DefaultActions=[{"Type": "forward", "TargetGroupArn": arn_d}],
+    )
+    groups = conn.describe_target_groups(LoadBalancerArn=lb_arn)["TargetGroups"]
+    assert len(groups) == 2
+    assert groups[0]["TargetGroupName"] == "a-target"
+    assert groups[1]["TargetGroupName"] == "d-target"
+
 
 @mock_elbv2
 @mock_ec2