diff --git a/packages/@aws-cdk/aws-ec2-alpha/lib/ipam.ts b/packages/@aws-cdk/aws-ec2-alpha/lib/ipam.ts index 378e9f9726291..10074ce6424fa 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/lib/ipam.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/lib/ipam.ts @@ -1,6 +1,6 @@ import { CfnIPAM, CfnIPAMPool, CfnIPAMPoolCidr, CfnIPAMScope } from 'aws-cdk-lib/aws-ec2'; import { Construct } from 'constructs'; -import { Lazy, Names, Resource, Stack, Tags } from 'aws-cdk-lib'; +import { Lazy, Names, Resource, Stack, Tags, Token } from 'aws-cdk-lib'; /** * Represents the address family for IP addresses in an IPAM pool. @@ -514,7 +514,7 @@ export class Ipam extends Resource { if (props?.ipamName) { Tags.of(this).add(NAME_TAG, props.ipamName); } - if (!props?.operatingRegion && !Stack.of(this).region) { + if (!props?.operatingRegion && Token.isUnresolved(Stack.of(this).region)) { throw new Error('Please provide at least one operating region'); } diff --git a/packages/@aws-cdk/aws-ec2-alpha/lib/route.ts b/packages/@aws-cdk/aws-ec2-alpha/lib/route.ts index b6f9eb1ee12e4..6967eb52fc58d 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/lib/route.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/lib/route.ts @@ -608,12 +608,8 @@ export class RouteTargetType { readonly endpoint?: IVpcEndpoint; constructor(props: RouteTargetProps) { - if ((props.gateway && props.endpoint) || (!props.gateway && !props.endpoint)) { - throw new Error('Exactly one of `gateway` or `endpoint` must be specified.'); - } else { - this.gateway = props.gateway; - this.endpoint = props.endpoint; - } + this.gateway = props.gateway; + this.endpoint = props.endpoint; } } @@ -732,6 +728,10 @@ export class Route extends Resource implements IRouteV2 { if (this.target.gateway?.routerType === RouterType.EGRESS_ONLY_INTERNET_GATEWAY && isDestinationIpv4) { throw new Error('Egress only internet gateway does not support IPv4 routing'); } + + if ((props.target.gateway && props.target.endpoint) || (!props.target.gateway && !props.target.endpoint)) { + throw new Error('Exactly one of `gateway` or `endpoint` must be specified.'); + } this.targetRouterType = this.target.gateway ? this.target.gateway.routerType : RouterType.VPC_ENDPOINT; // Gateway generates route automatically via its RouteTable, thus we don't need to generate the resource for it if (!(this.target.endpoint instanceof GatewayVpcEndpoint)) { diff --git a/packages/@aws-cdk/aws-ec2-alpha/test/ipam.test.ts b/packages/@aws-cdk/aws-ec2-alpha/test/ipam.test.ts index 78e681c5dc0f5..3b459bfeba8e8 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/test/ipam.test.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/test/ipam.test.ts @@ -155,4 +155,59 @@ describe('IPAM Test', () => { ); }); + test('IPAM throws error if awsService not provided with IPv6', () => { + // Create IPAM resources + const ipamRegion = new Ipam(stack, 'TestIpam', { + operatingRegion: ['us-west-2'], + }); + const poolOptions: vpc.PoolOptions = { + addressFamily: AddressFamily.IP_V6, + publicIpSource: IpamPoolPublicIpSource.AMAZON, + locale: 'us-west-2', + }; + expect(() => ipamRegion.publicScope.addPool('TestPool', poolOptions)).toThrow('awsService is required when addressFamily is set to ipv6'); + }); + + test('IPAM throws error if operating region is not provided', () => { + const app = new cdk.App(); + const stack_new = new cdk.Stack(app, 'TestStack', {}); + expect(() => new Ipam(stack_new, 'TestIpam')).toThrow('Please provide at least one operating region'); + }); + + test('IPAM does not throw error if stack region is not configured', () => { + const app = new cdk.App(); + const stack_new = new cdk.Stack(app, 'TestStack'); + new Ipam(stack_new, 'TestIpam', { + operatingRegion: ['us-west-2'], + }); + Template.fromStack(stack_new).hasResource( + 'AWS::EC2::IPAM', {}, + ); + }); + + test('IPAM does not throw error with operating region inferred from stack region', () => { + const app = new cdk.App(); + const stack_new = new cdk.Stack(app, 'TestStack', { + env: { + region: 'us-west-2', + }, + }); + new Ipam(stack_new, 'TestIpam', {}); + Template.fromStack(stack_new).hasResource( + 'AWS::EC2::IPAM', {}, + ); + }); + + test('IPAM throws error if locale is not in operating regions', () => { + const ipamRegion = new Ipam(stack, 'TestIpam', { + operatingRegion: ['us-west-2'], + }); + const poolOptions: vpc.PoolOptions = { + addressFamily: AddressFamily.IP_V6, + awsService: vpc.AwsServiceName.EC2, + publicIpSource: IpamPoolPublicIpSource.AMAZON, + locale: 'us-west-1', + }; + expect(() => ipamRegion.publicScope.addPool('TestPool', poolOptions)).toThrow("The provided locale 'us-west-1' is not in the operating regions."); + }); });// End Test diff --git a/packages/@aws-cdk/aws-ec2-alpha/test/route.test.ts b/packages/@aws-cdk/aws-ec2-alpha/test/route.test.ts index 0428e3458e352..55a2e9550bcba 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/test/route.test.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/test/route.test.ts @@ -3,7 +3,7 @@ import * as vpc from '../lib/vpc-v2'; import * as subnet from '../lib/subnet-v2'; import { CfnEIP, GatewayVpcEndpoint, GatewayVpcEndpointAwsService, SubnetType, VpnConnectionType } from 'aws-cdk-lib/aws-ec2'; import * as route from '../lib/route'; -import { Template } from 'aws-cdk-lib/assertions'; +import { Match, Template } from 'aws-cdk-lib/assertions'; describe('EC2 Routing', () => { let stack: cdk.Stack; @@ -405,6 +405,32 @@ describe('EC2 Routing', () => { }); }); + test('Throws error if VPC and allocationID is not provided', () => { + expect(() => new route.NatGateway(stack, 'TestNATGW', { + subnet: mySubnet, + connectivityType: route.NatConnectivityType.PUBLIC, + maxDrainDuration: cdk.Duration.seconds(2001), + })).toThrow('Either provide vpc or allocationId'); + }); + + test('does not create EIP or use allocationId for private NAT gateway', () => { + // WHEN + new route.NatGateway(stack, 'NGW', { + vpc: myVpc, + subnet: mySubnet, + connectivityType: route.NatConnectivityType.PRIVATE, + }); + + // THEN + Template.fromStack(stack).resourceCountIs('AWS::EC2::EIP', 0); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::NatGateway', { + ConnectivityType: 'private', + }); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::NatGateway', Match.not({ + AllocationId: Match.anyValue(), + })); + }); + test('Route to DynamoDB Endpoint', () => { const dynamodb = new GatewayVpcEndpoint(stack, 'TestDB', { vpc: myVpc, @@ -512,6 +538,40 @@ describe('EC2 Routing', () => { }, }); }); + + test('Route throws error if no target is specified', () => { + expect(() => { + routeTable.addRoute('testRoute', '0.0.0.0', {}); + }).toThrow('Exactly one of `gateway` or `endpoint` must be specified.'); + }); + + test('Route throws error if both endpoint and gateway as target is specified', () => { + const eigw = new route.EgressOnlyInternetGateway(stack, 'TestEIGW', { + vpc: myVpc, + }); + + const dynamodb = new GatewayVpcEndpoint(stack, 'TestDB', { + vpc: myVpc, + service: GatewayVpcEndpointAwsService.DYNAMODB, + }); + expect(() => { + routeTable.addRoute('testRoute', '::/0', { + gateway: eigw, + endpoint: dynamodb, + }); + }).toThrow('Exactly one of `gateway` or `endpoint` must be specified.'); + }); + + test('EIGW throws error for IPv4 routing', () => { + const eigw = new route.EgressOnlyInternetGateway(stack, 'TestEIGW', { + vpc: myVpc, + }); + expect(() => { + routeTable.addRoute('testRoute', '0.0.0.0', { + gateway: eigw, + }); + }).toThrow('Egress only internet gateway does not support IPv4 routing'); + }); }); describe('VPCPeeringConnection', () => { @@ -523,6 +583,7 @@ describe('VPCPeeringConnection', () => { let vpcA: vpc.VpcV2; let vpcB: vpc.VpcV2; let vpcC: vpc.VpcV2; + let vpcD: vpc.VpcV2; beforeEach(() => { const app = new cdk.App({ @@ -545,7 +606,30 @@ describe('VPCPeeringConnection', () => { vpcC = new vpc.VpcV2(stackC, 'VpcC', { primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), }); + //Same Account VPC + vpcD = new vpc.VpcV2(stackC, 'VpcD', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.3.0.0/16'), + }); + + }); + test('Creates a same account VPC peering connection', () => { + // Create VPC peering connection between vpcA and vpcB in same account + new route.VPCPeeringConnection(stackC, 'TestPeeringConnection', { + requestorVpc: vpcC, + acceptorVpc: vpcD, + }); + + const template = Template.fromStack(stackC); + template.hasResourceProperties('AWS::EC2::VPCPeeringConnection', { + VpcId: { + 'Fn::GetAtt': ['VpcC211819BA', 'VpcId'], + }, + PeerVpcId: { + 'Fn::GetAtt': ['VpcD66D5BFD0', 'VpcId'], // Replace ... with actual ID suffix + }, + PeerRegion: 'us-west-2', + }); }); test('Creates a cross account VPC peering connection', () => { @@ -628,15 +712,34 @@ describe('VPCPeeringConnection', () => { }); test('CIDR block overlap with primary CIDR block should throw error', () => { - const vpcD = new vpc.VpcV2(stackA, 'VpcD', { + const testVpc = new vpc.VpcV2(stackA, 'TestVpc', { primaryAddressBlock: vpc.IpAddresses.ipv4('10.0.0.0/16'), }); - expect(() => { new route.VPCPeeringConnection(stackA, 'TestPeering', { requestorVpc: vpcA, - acceptorVpc: vpcD, + acceptorVpc: testVpc, }); }).toThrow(/CIDR block should not overlap with each other for establishing a peering connection/); }); + + test('Can create route for VPC peering connection as a target', () => { + const peering = new route.VPCPeeringConnection(stackC, 'TestPeering', { + requestorVpc: vpcC, + acceptorVpc: vpcD, + }); + + const routeTable = new route.RouteTable(stackC, 'TestRouteTable', { + vpc: vpcD, + }); + + routeTable.addRoute('TestRoute', '172.16.0.0/16', { + gateway: peering, + }); + const template = Template.fromStack(stackC); + template.hasResourceProperties('AWS::EC2::Route', { + VpcPeeringConnectionId: { 'Fn::GetAtt': ['TestPeeringVPCPeeringConnection0E2D1596', 'Id'] }, + RouteTableId: { 'Fn::GetAtt': ['TestRouteTableC34C2E1C', 'RouteTableId'] }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-ec2-alpha/test/subnet-v2.test.ts b/packages/@aws-cdk/aws-ec2-alpha/test/subnet-v2.test.ts index a78c1c3806a1a..7ab6640964295 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/test/subnet-v2.test.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/test/subnet-v2.test.ts @@ -322,4 +322,62 @@ describe('Subnet V2 with custom IP and routing', () => { expect(Template.fromStack(stack).hasResource('AWS::EC2::SubnetNetworkAclAssociation', {})); }); + + test('Subnet Creation throws error if assignIPv6 set to true with no IPv6 CIDR', () => { + const testVpc = new vpc.VpcV2(stack, 'TestVPC', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), + secondaryAddressBlocks: [vpc.IpAddresses.amazonProvidedIpv6( + { cidrBlockName: 'SecondaryAddress' })], + }); + + const subnetConfig = { + vpcV2: testVpc, + availabilityZone: 'us-east-1a', + cidrBlock: new subnet.IpCidr('10.1.0.0/24'), + subnetType: SubnetType.PUBLIC, + assignIpv6AddressOnCreation: true, + }; + expect(() => createTestSubnet(stack, subnetConfig)).toThrow('IPv6 CIDR block is required when assigning IPv6 address on creation'); + }); + + test('Subnet Creation does not throw error if assignIPv6 set to true with no IPv6 CIDR', () => { + const testVpc = new vpc.VpcV2(stack, 'TestVPC', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), + secondaryAddressBlocks: [vpc.IpAddresses.amazonProvidedIpv6( + { cidrBlockName: 'SecondaryAddress' })], + }); + + const subnetConfig = { + vpcV2: testVpc, + availabilityZone: 'us-east-1a', + cidrBlock: new subnet.IpCidr('10.1.0.0/24'), + ipv6Cidr: new subnet.IpCidr('2001:db8:1::/64'), + subnetType: SubnetType.PUBLIC, + assignIpv6AddressOnCreation: true, + }; + createTestSubnet(stack, subnetConfig); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::Subnet', { + AssignIpv6AddressOnCreation: true, + Ipv6CidrBlock: '2001:db8:1::/64', + }); + }); + + test('Subnet creates custom route Table if not provided', () => { + const testVpc = new vpc.VpcV2(stack, 'TestVPC', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), + }); + const subnetConfig = { + vpcV2: testVpc, + availabilityZone: 'us-east-1a', + cidrBlock: new subnet.IpCidr('10.1.0.0/24'), + subnetType: SubnetType.PUBLIC, + }; + createTestSubnet(stack, subnetConfig); + Template.fromStack(stack).hasResourceProperties('AWS::EC2::SubnetRouteTableAssociation', { + SubnetId: { + Ref: 'TestSubnet2A4BE4CA', + }, + RouteTableId: { 'Fn::GetAtt': ['TestSubnetRouteTable5AF4379E', 'RouteTableId'] }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-ec2-alpha/test/util.test.ts b/packages/@aws-cdk/aws-ec2-alpha/test/util.test.ts index 56764458f3044..b132db57fe1ea 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/test/util.test.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/test/util.test.ts @@ -1,4 +1,4 @@ -import { CidrBlock, CidrBlockIpv6 } from '../lib/util'; +import { CidrBlock, CidrBlockIpv6, NetworkUtils } from '../lib/util'; describe('Tests for the CidrBlock.rangesOverlap method to check if IPv4 ranges overlap', () =>{ test('Should return false for non-overlapping IP ranges', () => { @@ -45,4 +45,29 @@ describe('Tests for the CidrBlock.rangesOverlap method to check if IPv4 ranges o const testCidr = new CidrBlockIpv6('2001:db8::/32'); expect(testCidr.rangesOverlap('2001:db8::1/64', '2001:db8::1/60')).toBe(true); }); + + test('valid IP addresses return true', () => { + const validIps = [ + '192.168.1.1', + '10.0.0.0', + '172.16.254.1', + '0.0.0.0', + '255.255.255.255', + ]; + + validIps.forEach(ip => { + expect(NetworkUtils.validIp(ip)).toBe(true); + }); + }); + + test('invalid IP addresses return false', () => { + const invalidIps = [ + '256.1.2.3', // octet > 255 + '1.2.3.256', // octet > 255 + '1.2.3.4.5', + ]; + invalidIps.forEach(ip => { + expect(NetworkUtils.validIp(ip)).toBe(false); + }); // too many octets + }); }); diff --git a/packages/@aws-cdk/aws-ec2-alpha/test/vpc-v2.test.ts b/packages/@aws-cdk/aws-ec2-alpha/test/vpc-v2.test.ts index beb8fcda41c79..6a12944ae2d80 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/test/vpc-v2.test.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/test/vpc-v2.test.ts @@ -305,4 +305,63 @@ describe('Vpc V2 with full control', () => { const template = Template.fromStack(stack); template.resourceCountIs('AWS::EC2::VPCCidrBlock', 2); }); + + test('Set IPv6 enabled to true if secondary address defined using IPAM Ipv6', () => { + const ipv6Vpc = new vpc.VpcV2(stack, 'TestVpc', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), + secondaryAddressBlocks: [vpc.IpAddresses.ipv6Ipam({ + ipamPool: new Ipam(stack, 'TestIpam', { + operatingRegion: ['us-west-1'], + }).publicScope.addPool('PublicPool0', { + addressFamily: AddressFamily.IP_V6, + awsService: AwsServiceName.EC2, + publicIpSource: IpamPoolPublicIpSource.AMAZON, + locale: 'us-west-1', + }), + netmaskLength: 64, + cidrBlockName: 'IPv6Ipam', + })], + }, + ); + expect(ipv6Vpc.useIpv6).toBe(true); + }); + + test('Set IPv6 enabled to true if secondary address defined using Amazon Provided IPv6', () => { + const ipv6Vpc = new vpc.VpcV2(stack, 'TestVpc', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), + secondaryAddressBlocks: [vpc.IpAddresses.amazonProvidedIpv6({ + cidrBlockName: 'Ipv6Amazon', + })], + }, + ); + expect(ipv6Vpc.useIpv6).toBe(true); + }); + test('Set IPv6 enabled to true if secondary address defined using BYOIP IPv6', () => { + const ipv6Vpc = new vpc.VpcV2(stack, 'TestVpc', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), + secondaryAddressBlocks: [vpc.IpAddresses.ipv6ByoipPool({ + ipv6PoolId: 'test-Byoip-pool', + ipv6CidrBlock: '2001:db8::/32', + cidrBlockName: 'BYOIP', + })], + }, + ); + expect(ipv6Vpc.useIpv6).toBe(true); + }); + test('Set IPv6 enabled to false if secondary address is not defined as IPv6', () => { + const ipv6Vpc = new vpc.VpcV2(stack, 'TestVpc', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), + secondaryAddressBlocks: [vpc.IpAddresses.ipv4('10.2.0.0/16', { + cidrBlockName: 'SecondaryAddress', + })], + }, + ); + expect(ipv6Vpc.useIpv6).toBe(false); + }); + test('VPC throws error is secondary CIDR block name is not provided', () => { + expect(() => new vpc.VpcV2(stack, 'TestVPC', { + primaryAddressBlock: vpc.IpAddresses.ipv4('10.1.0.0/16'), + secondaryAddressBlocks: [vpc.IpAddresses.ipv4('10.2.0.0/16')], + })).toThrow('Cidr Block Name is required to create secondary IP address'); + }); }); diff --git a/packages/@aws-cdk/aws-ec2-alpha/test/vpcv2-import.test.ts b/packages/@aws-cdk/aws-ec2-alpha/test/vpcv2-import.test.ts index 212eb12a47af5..8388964efae61 100644 --- a/packages/@aws-cdk/aws-ec2-alpha/test/vpcv2-import.test.ts +++ b/packages/@aws-cdk/aws-ec2-alpha/test/vpcv2-import.test.ts @@ -214,4 +214,134 @@ describe('Vpc V2 with full control', () => { SubnetId: 'mockSubnetId', }); }); + test('correctly categorizes imported subnets by type', () => { + + const vpc = VpcV2.fromVpcV2Attributes(stack, 'ImportedVPC', { + vpcId: 'vpc-123456', + vpcCidrBlock: '10.0.0.0/16', + subnets: [ + { + subnetId: 'subnet-private1', + availabilityZone: 'us-east-1a', + ipv4CidrBlock: '10.0.1.0/24', + routeTableId: 'rt-private1', + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + subnetName: 'Private1', + }, + { + subnetId: 'subnet-private2', + availabilityZone: 'us-east-1b', + ipv4CidrBlock: '10.0.2.0/24', + routeTableId: 'rt-private2', + subnetType: SubnetType.PRIVATE_WITH_NAT, + subnetName: 'Private2', + }, + { + subnetId: 'subnet-deprecated', + availabilityZone: 'us-east-1a', + ipv4CidrBlock: '10.0.3.0/24', + routeTableId: 'rt-deprecated', + subnetType: 'Deprecated_Private' as any, + subnetName: 'DeprecatedPrivate', + }, + { + subnetId: 'subnet-public1', + availabilityZone: 'us-east-1a', + ipv4CidrBlock: '10.0.4.0/24', + routeTableId: 'rt-public1', + subnetType: SubnetType.PUBLIC, + subnetName: 'Public1', + }, + { + subnetId: 'subnet-isolated1', + availabilityZone: 'us-east-1b', + ipv4CidrBlock: '10.0.5.0/24', + routeTableId: 'rt-isolated1', + subnetType: SubnetType.PRIVATE_ISOLATED, + subnetName: 'Isolated1', + }, + { + subnetId: 'subnet-deprecated-isolated', + availabilityZone: 'us-east-1b', + ipv4CidrBlock: '10.0.6.0/24', + routeTableId: 'rt-deprecated-isolated', + subnetType: 'Deprecated_Isolated' as any, + subnetName: 'DeprecatedIsolated', + }, + ], + }); + + // Verify private subnets + expect(vpc.privateSubnets.length).toBe(3); + expect(vpc.privateSubnets[0].subnetId).toBe('subnet-private1'); + expect(vpc.privateSubnets[1].subnetId).toBe('subnet-private2'); + expect(vpc.privateSubnets[2].subnetId).toBe('subnet-deprecated'); + + // Verify public subnets + expect(vpc.publicSubnets.length).toBe(1); + expect(vpc.publicSubnets[0].subnetId).toBe('subnet-public1'); + + // Verify isolated subnets + expect(vpc.isolatedSubnets.length).toBe(2); + expect(vpc.isolatedSubnets[0].subnetId).toBe('subnet-isolated1'); + expect(vpc.isolatedSubnets[1].subnetId).toBe('subnet-deprecated-isolated'); + }); + + test('uses default names for subnets without names', () => { + const vpc = VpcV2.fromVpcV2Attributes(stack, 'ImportedVPC', { + vpcId: 'vpc-123456', + vpcCidrBlock: '10.0.0.0/16', + subnets: [ + { + subnetId: 'subnet-private1', + availabilityZone: 'us-east-1a', + ipv4CidrBlock: '10.0.1.0/24', + routeTableId: 'rt-private1', + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + }, + { + subnetId: 'subnet-public1', + availabilityZone: 'us-east-1a', + ipv4CidrBlock: '10.0.2.0/24', + routeTableId: 'rt-public1', + subnetType: SubnetType.PUBLIC, + }, + { + subnetId: 'subnet-isolated1', + availabilityZone: 'us-east-1a', + ipv4CidrBlock: '10.0.3.0/24', + routeTableId: 'rt-isolated1', + subnetType: SubnetType.PRIVATE_ISOLATED, + }, + ], + }); + + // Verify default names are used + expect(vpc.privateSubnets[0].node.id).toBe('ImportedPrivateSubnet'); + expect(vpc.publicSubnets[0].node.id).toBe('ImportedPublicSubnet'); + expect(vpc.isolatedSubnets[0].node.id).toBe('ImportedIsolatedSubnet'); + }); + + test('handles empty subnets array', () => { + const vpc = VpcV2.fromVpcV2Attributes(stack, 'ImportedVPC', { + vpcId: 'vpc-123456', + vpcCidrBlock: '10.0.0.0/16', + subnets: [], + }); + + expect(vpc.privateSubnets).toHaveLength(0); + expect(vpc.publicSubnets).toHaveLength(0); + expect(vpc.isolatedSubnets).toHaveLength(0); + }); + + test('handles undefined subnets', () => { + const vpc = VpcV2.fromVpcV2Attributes(stack, 'ImportedVPC', { + vpcId: 'vpc-123456', + vpcCidrBlock: '10.0.0.0/16', + }); + + expect(vpc.privateSubnets).toHaveLength(0); + expect(vpc.publicSubnets).toHaveLength(0); + expect(vpc.isolatedSubnets).toHaveLength(0); + }); });