Repository: getmoto/moto
Pull request #7679 (2024-05-07 15:25:57)

EC2: Add test for DescribeNetworkAcls using the association.subnet-id filter
I attempted to add coverage to DescribeNetworkAcls when using the `association.subnet-id` filter (since [this issue](https://github.com/localstack/localstack/issues/10097) was reported a while ago) to ensure it works as expected.

This also fixes a small bug where network ACLs could be deleted even when associated to subnets (according to [AWS's documentation](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DeleteNetworkAcl.html), this cannot happen).


Patch:
diff --git a/moto/ec2/models/network_acls.py b/moto/ec2/models/network_acls.py
index 5e9808778..aa11829e6 100644
--- a/moto/ec2/models/network_acls.py
+++ b/moto/ec2/models/network_acls.py
@@ -1,6 +1,7 @@
 from typing import Any, Dict, List, Optional
 
 from ..exceptions import (
+    DependencyViolationError,
     InvalidNetworkAclIdError,
     InvalidRouteTableIdError,
     NetworkAclEntryAlreadyExistsError,
@@ -61,6 +62,14 @@ class NetworkAclBackend:
             )
 
     def delete_network_acl(self, network_acl_id: str) -> "NetworkAcl":
+        if any(
+            network_acl.id == network_acl_id and len(network_acl.associations) > 0
+            for network_acl in self.network_acls.values()
+        ):
+            raise DependencyViolationError(
+                f"The network ACL '{network_acl_id}' has dependencies and cannot be deleted."
+            )
+
         deleted = self.network_acls.pop(network_acl_id, None)
         if not deleted:
             raise InvalidNetworkAclIdError(network_acl_id)
diff --git a/moto/ec2/models/route_tables.py b/moto/ec2/models/route_tables.py
index c063b359c..c557b5a70 100644
--- a/moto/ec2/models/route_tables.py
+++ b/moto/ec2/models/route_tables.py
@@ -116,6 +116,12 @@ class RouteTable(TaggedEC2Resource, CloudFormationModel):
                 for route in self.routes.values()
                 if route.vpc_pcx is not None
             ]
+        elif filter_name == "route.nat-gateway-id":
+            return [
+                route.nat_gateway.id
+                for route in self.routes.values()
+                if route.nat_gateway is not None
+            ]
         else:
             return super().get_filter_value(filter_name, "DescribeRouteTables")
 


Test patch:
diff --git a/.github/workflows/tests_sdk_ruby.yml b/.github/workflows/tests_sdk_ruby.yml
index fea7721c7..749ab8763 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@1198b074305f9356bd56dd4b311757cc0dab2f1c
+        uses: ruby/setup-ruby@cacc9f1c0b3f4eb8a16a6bb0ed10897b43b9de49
         with:
           ruby-version: ${{ matrix.ruby-version }}
       - name: Set up Python 3.8
diff --git a/tests/test_ec2/test_network_acls.py b/tests/test_ec2/test_network_acls.py
index f861974f2..bde8c726c 100644
--- a/tests/test_ec2/test_network_acls.py
+++ b/tests/test_ec2/test_network_acls.py
@@ -421,6 +421,94 @@ def test_describe_network_acls():
     )
 
 
+@mock_aws
+def test_describe_network_acls_associations_by_subnet():
+    ec2_client = boto3.client("ec2", region_name="us-west-2")
+    vpc = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")
+    vpc_id = vpc["Vpc"]["VpcId"]
+    all_acls = ec2_client.describe_network_acls(
+        Filters=[{"Name": "vpc-id", "Values": [vpc_id]}]
+    )["NetworkAcls"]
+    subnet_1_id = ec2_client.create_subnet(VpcId=vpc_id, CidrBlock="10.0.1.0/24")[
+        "Subnet"
+    ]["SubnetId"]
+    subnet_2_id = ec2_client.create_subnet(VpcId=vpc_id, CidrBlock="10.0.2.0/24")[
+        "Subnet"
+    ]["SubnetId"]
+    subnet_3_id = ec2_client.create_subnet(VpcId=vpc_id, CidrBlock="10.0.3.0/24")[
+        "Subnet"
+    ]["SubnetId"]
+    default_acl_id = next(acl["NetworkAclId"] for acl in all_acls if acl["IsDefault"])
+    custom_acl_id = ec2_client.create_network_acl(VpcId=vpc_id)["NetworkAcl"][
+        "NetworkAclId"
+    ]
+
+    # Subnets should be associated with the default ACL by default
+    assert_subnet_is_associated_to_acl(ec2_client, subnet_1_id, default_acl_id)
+    assert_subnet_is_associated_to_acl(ec2_client, subnet_3_id, default_acl_id)
+    assert_subnet_is_associated_to_acl(ec2_client, subnet_2_id, default_acl_id)
+
+    associate_subnet_to_acl(ec2_client, subnet_1_id, custom_acl_id)
+    associate_subnet_to_acl(ec2_client, subnet_3_id, custom_acl_id)
+    assert_subnet_is_associated_to_acl(ec2_client, subnet_1_id, custom_acl_id)
+    assert_subnet_is_associated_to_acl(ec2_client, subnet_3_id, custom_acl_id)
+    assert_subnet_is_associated_to_acl(ec2_client, subnet_2_id, default_acl_id)
+
+    # Re-associate Subnet 1 with the default ACL to test re-association
+    associate_subnet_to_acl(ec2_client, subnet_1_id, default_acl_id)
+    assert_subnet_is_associated_to_acl(ec2_client, subnet_1_id, default_acl_id)
+    assert_subnet_is_associated_to_acl(ec2_client, subnet_2_id, default_acl_id)
+    assert_subnet_is_associated_to_acl(ec2_client, subnet_3_id, custom_acl_id)
+
+    # Attempting to associate a non-existent ACL with a subnet should raise an error
+    with pytest.raises(ClientError):
+        associate_subnet_to_acl(ec2_client, subnet_1_id, "non-existent-acl")
+
+    # Attempting to delete a custom network ACL should fail if it has any subnets associated
+    with pytest.raises(ClientError) as exc:
+        ec2_client.delete_network_acl(NetworkAclId=custom_acl_id)
+    err = exc.value.response["Error"]
+    assert err["Code"] == "DependencyViolation"
+    assert (
+        err["Message"]
+        == f"The network ACL '{custom_acl_id}' has dependencies and cannot be deleted."
+    )
+
+    # Re-associate Subnet 3 with the default ACL to test deletion of custom ACL
+    associate_subnet_to_acl(ec2_client, subnet_3_id, default_acl_id)
+    ec2_client.delete_network_acl(NetworkAclId=custom_acl_id)
+    assert_subnet_is_associated_to_acl(ec2_client, subnet_3_id, default_acl_id)
+
+
+def associate_subnet_to_acl(ec2_client, subnet_id: str, acl_id: str):
+    assoc_id = get_subnet_acl_association_id(ec2_client, subnet_id)
+    ec2_client.replace_network_acl_association(
+        NetworkAclId=acl_id, AssociationId=assoc_id
+    )
+
+
+def get_subnet_acl_association_id(ec2_client, subnet_id: str):
+    network_acls_description = ec2_client.describe_network_acls(
+        Filters=[{"Name": "association.subnet-id", "Values": [subnet_id]}]
+    )
+    associations = network_acls_description["NetworkAcls"][0]["Associations"]
+    return next(
+        assoc["NetworkAclAssociationId"]
+        for assoc in associations
+        if assoc["SubnetId"] == subnet_id
+    )
+
+
+def assert_subnet_is_associated_to_acl(ec2_client, subnet_id: str, acl_id: str):
+    network_acl_description = ec2_client.describe_network_acls(
+        Filters=[{"Name": "association.subnet-id", "Values": [subnet_id]}]
+    )
+    acls_subnet = network_acl_description["NetworkAcls"]
+    # There should be only one ACL associated with the subnet at any given time
+    assert len(acls_subnet) == 1
+    assert acls_subnet[0]["NetworkAclId"] == acl_id
+
+
 @mock_aws
 def test_create_network_acl_with_tags():
     conn = boto3.client("ec2", region_name="us-west-2")
diff --git a/tests/test_ec2/test_route_tables.py b/tests/test_ec2/test_route_tables.py
index dc13d84f3..4187103c4 100644
--- a/tests/test_ec2/test_route_tables.py
+++ b/tests/test_ec2/test_route_tables.py
@@ -919,6 +919,47 @@ def test_describe_route_tables_with_nat_gateway():
     assert nat_gw_routes[0]["State"] == "active"
 
 
+@mock_aws
+def test_describe_route_tables_filter_with_nat_gateway_id():
+    ec2 = boto3.client("ec2", region_name="us-west-1")
+    vpc_id = ec2.create_vpc(CidrBlock="192.168.0.0/23")["Vpc"]["VpcId"]
+    az = ec2.describe_availability_zones()["AvailabilityZones"][0]["ZoneName"]
+    sn_id = ec2.create_subnet(
+        AvailabilityZone=az, CidrBlock="192.168.0.0/24", VpcId=vpc_id
+    )["Subnet"]["SubnetId"]
+    route_table_id = ec2.create_route_table(VpcId=vpc_id)["RouteTable"]["RouteTableId"]
+    ec2.associate_route_table(SubnetId=sn_id, RouteTableId=route_table_id)
+    alloc_id = ec2.allocate_address(Domain="vpc")["AllocationId"]
+    nat_gw_id = ec2.create_nat_gateway(SubnetId=sn_id, AllocationId=alloc_id)[
+        "NatGateway"
+    ]["NatGatewayId"]
+
+    route_tables_by_nat_gateway_id = ec2.describe_route_tables(
+        Filters=[{"Name": "route.nat-gateway-id", "Values": [nat_gw_id]}]
+    )["RouteTables"]
+    assert len(route_tables_by_nat_gateway_id) == 0
+
+    ec2.create_route(
+        DestinationCidrBlock="0.0.0.0/0",
+        NatGatewayId=nat_gw_id,
+        RouteTableId=route_table_id,
+    )
+
+    route_tables_by_nat_gateway_id = ec2.describe_route_tables(
+        Filters=[{"Name": "route.nat-gateway-id", "Values": [nat_gw_id]}]
+    )["RouteTables"][0]
+    nat_gw_routes = [
+        route
+        for route in route_tables_by_nat_gateway_id["Routes"]
+        if route["DestinationCidrBlock"] == "0.0.0.0/0"
+    ]
+
+    assert len(nat_gw_routes) == 1
+    assert nat_gw_routes[0]["DestinationCidrBlock"] == "0.0.0.0/0"
+    assert nat_gw_routes[0]["NatGatewayId"] == nat_gw_id
+    assert nat_gw_routes[0]["State"] == "active"
+
+
 @mock_aws
 def test_create_vpc_end_point():
     ec2 = boto3.client("ec2", region_name="us-west-1")