From 8ea40a1973ec1a07a6c1b83de347d293f190bc43 Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Fri, 27 Mar 2026 09:04:21 +1000 Subject: [PATCH] fix: test coverage --- ...scribeInstanceCreditSpecifications_test.go | 55 ++++ spinifex/gateway/ec2/natgw/natgw_test.go | 47 +++ .../ModifyNetworkInterfaceAttribute_test.go | 34 ++ .../handlers/ec2/instance/helpers_test.go | 33 ++ spinifex/handlers/ec2/natgw/helpers_test.go | 69 ++++ .../handlers/ec2/routetable/helpers_test.go | 245 +++++++++++++++ spinifex/handlers/ec2/vpc/eni_mac_test.go | 32 ++ spinifex/handlers/elbv2/arn_test.go | 36 +++ spinifex/handlers/elbv2/coverage_test.go | 297 ++++++++++++++++++ spinifex/services/vpcd/helpers_test.go | 77 +++++ 10 files changed, 925 insertions(+) create mode 100644 spinifex/gateway/ec2/instance/DescribeInstanceCreditSpecifications_test.go create mode 100644 spinifex/gateway/ec2/natgw/natgw_test.go create mode 100644 spinifex/gateway/ec2/vpc/ModifyNetworkInterfaceAttribute_test.go create mode 100644 spinifex/handlers/ec2/instance/helpers_test.go create mode 100644 spinifex/handlers/ec2/natgw/helpers_test.go create mode 100644 spinifex/handlers/ec2/routetable/helpers_test.go create mode 100644 spinifex/handlers/ec2/vpc/eni_mac_test.go create mode 100644 spinifex/handlers/elbv2/arn_test.go create mode 100644 spinifex/handlers/elbv2/coverage_test.go create mode 100644 spinifex/services/vpcd/helpers_test.go diff --git a/spinifex/gateway/ec2/instance/DescribeInstanceCreditSpecifications_test.go b/spinifex/gateway/ec2/instance/DescribeInstanceCreditSpecifications_test.go new file mode 100644 index 000000000..9581cc0bb --- /dev/null +++ b/spinifex/gateway/ec2/instance/DescribeInstanceCreditSpecifications_test.go @@ -0,0 +1,55 @@ +package gateway_ec2_instance + +import ( + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/mulgadc/spinifex/spinifex/awserrors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDescribeInstanceCreditSpecifications_NilInput(t *testing.T) { + out, err := DescribeInstanceCreditSpecifications(nil) + require.Error(t, err) + assert.Nil(t, out) + assert.Equal(t, awserrors.ErrorInvalidParameterValue, err.Error()) +} + +func TestDescribeInstanceCreditSpecifications_EmptyInstanceIds(t *testing.T) { + out, err := DescribeInstanceCreditSpecifications(&ec2.DescribeInstanceCreditSpecificationsInput{}) + require.NoError(t, err) + assert.Empty(t, out.InstanceCreditSpecifications) +} + +func TestDescribeInstanceCreditSpecifications_SingleInstance(t *testing.T) { + out, err := DescribeInstanceCreditSpecifications(&ec2.DescribeInstanceCreditSpecificationsInput{ + InstanceIds: []*string{aws.String("i-abc123")}, + }) + require.NoError(t, err) + require.Len(t, out.InstanceCreditSpecifications, 1) + assert.Equal(t, "i-abc123", *out.InstanceCreditSpecifications[0].InstanceId) + assert.Equal(t, "standard", *out.InstanceCreditSpecifications[0].CpuCredits) +} + +func TestDescribeInstanceCreditSpecifications_MultipleInstances(t *testing.T) { + out, err := DescribeInstanceCreditSpecifications(&ec2.DescribeInstanceCreditSpecificationsInput{ + InstanceIds: []*string{aws.String("i-aaa"), aws.String("i-bbb"), aws.String("i-ccc")}, + }) + require.NoError(t, err) + require.Len(t, out.InstanceCreditSpecifications, 3) + for _, spec := range out.InstanceCreditSpecifications { + assert.Equal(t, "standard", *spec.CpuCredits) + } +} + +func TestDescribeInstanceCreditSpecifications_SkipsNilAndEmpty(t *testing.T) { + out, err := DescribeInstanceCreditSpecifications(&ec2.DescribeInstanceCreditSpecificationsInput{ + InstanceIds: []*string{nil, aws.String(""), aws.String("i-valid"), nil, aws.String("i-also")}, + }) + require.NoError(t, err) + require.Len(t, out.InstanceCreditSpecifications, 2) + assert.Equal(t, "i-valid", *out.InstanceCreditSpecifications[0].InstanceId) + assert.Equal(t, "i-also", *out.InstanceCreditSpecifications[1].InstanceId) +} diff --git a/spinifex/gateway/ec2/natgw/natgw_test.go b/spinifex/gateway/ec2/natgw/natgw_test.go new file mode 100644 index 000000000..ae7b887fe --- /dev/null +++ b/spinifex/gateway/ec2/natgw/natgw_test.go @@ -0,0 +1,47 @@ +package gateway_ec2_natgw + +import ( + "testing" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/mulgadc/spinifex/spinifex/awserrors" + "github.com/stretchr/testify/assert" +) + +const testAccountID = "123456789012" + +// CreateNatGateway tests + +func TestCreateNatGateway_NilInput(t *testing.T) { + _, err := CreateNatGateway(nil, nil, testAccountID) + assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue) +} + +func TestCreateNatGateway_NilNATS(t *testing.T) { + _, err := CreateNatGateway(&ec2.CreateNatGatewayInput{}, nil, testAccountID) + assert.Error(t, err) +} + +// DeleteNatGateway tests + +func TestDeleteNatGateway_NilInput(t *testing.T) { + _, err := DeleteNatGateway(nil, nil, testAccountID) + assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue) +} + +func TestDeleteNatGateway_NilNATS(t *testing.T) { + _, err := DeleteNatGateway(&ec2.DeleteNatGatewayInput{}, nil, testAccountID) + assert.Error(t, err) +} + +// DescribeNatGateways tests + +func TestDescribeNatGateways_NilInput(t *testing.T) { + _, err := DescribeNatGateways(nil, nil, testAccountID) + assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue) +} + +func TestDescribeNatGateways_NilNATS(t *testing.T) { + _, err := DescribeNatGateways(&ec2.DescribeNatGatewaysInput{}, nil, testAccountID) + assert.Error(t, err) +} diff --git a/spinifex/gateway/ec2/vpc/ModifyNetworkInterfaceAttribute_test.go b/spinifex/gateway/ec2/vpc/ModifyNetworkInterfaceAttribute_test.go new file mode 100644 index 000000000..2012dc99a --- /dev/null +++ b/spinifex/gateway/ec2/vpc/ModifyNetworkInterfaceAttribute_test.go @@ -0,0 +1,34 @@ +package gateway_ec2_vpc + +import ( + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/mulgadc/spinifex/spinifex/awserrors" + "github.com/stretchr/testify/assert" +) + +func TestModifyNetworkInterfaceAttribute_NilInput(t *testing.T) { + _, err := ModifyNetworkInterfaceAttribute(nil, nil, "123456789012") + assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue) +} + +func TestModifyNetworkInterfaceAttribute_NilNetworkInterfaceId(t *testing.T) { + _, err := ModifyNetworkInterfaceAttribute(&ec2.ModifyNetworkInterfaceAttributeInput{}, nil, "123456789012") + assert.EqualError(t, err, awserrors.ErrorMissingParameter) +} + +func TestModifyNetworkInterfaceAttribute_EmptyNetworkInterfaceId(t *testing.T) { + _, err := ModifyNetworkInterfaceAttribute(&ec2.ModifyNetworkInterfaceAttributeInput{ + NetworkInterfaceId: aws.String(""), + }, nil, "123456789012") + assert.EqualError(t, err, awserrors.ErrorMissingParameter) +} + +func TestModifyNetworkInterfaceAttribute_NilNATS(t *testing.T) { + _, err := ModifyNetworkInterfaceAttribute(&ec2.ModifyNetworkInterfaceAttributeInput{ + NetworkInterfaceId: aws.String("eni-abc123"), + }, nil, "123456789012") + assert.Error(t, err) +} diff --git a/spinifex/handlers/ec2/instance/helpers_test.go b/spinifex/handlers/ec2/instance/helpers_test.go new file mode 100644 index 000000000..d80b28202 --- /dev/null +++ b/spinifex/handlers/ec2/instance/helpers_test.go @@ -0,0 +1,33 @@ +package handlers_ec2_instance + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// --- generateNetworkConfig --- + +func TestGenerateNetworkConfig_BothEmpty(t *testing.T) { + cfg := generateNetworkConfig("", "") + assert.Equal(t, cloudInitNetworkConfigWildcard, cfg) +} + +func TestGenerateNetworkConfig_OneEmpty(t *testing.T) { + cfg := generateNetworkConfig("02:00:00:aa:bb:cc", "") + assert.Equal(t, cloudInitNetworkConfigWildcard, cfg, "should fall back to wildcard if devMAC empty") + + cfg = generateNetworkConfig("", "02:00:00:dd:ee:ff") + assert.Equal(t, cloudInitNetworkConfigWildcard, cfg, "should fall back to wildcard if eniMAC empty") +} + +func TestGenerateNetworkConfig_DualNIC(t *testing.T) { + cfg := generateNetworkConfig("02:00:00:aa:bb:cc", "02:00:00:dd:ee:ff") + assert.Contains(t, cfg, "version: 2") + assert.Contains(t, cfg, `macaddress: "02:00:00:aa:bb:cc"`) + assert.Contains(t, cfg, `macaddress: "02:00:00:dd:ee:ff"`) + assert.Contains(t, cfg, "use-routes: false") + assert.Contains(t, cfg, "use-dns: false") + assert.Contains(t, cfg, "vpc0:") + assert.Contains(t, cfg, "dev0:") +} diff --git a/spinifex/handlers/ec2/natgw/helpers_test.go b/spinifex/handlers/ec2/natgw/helpers_test.go new file mode 100644 index 000000000..3340bf1f5 --- /dev/null +++ b/spinifex/handlers/ec2/natgw/helpers_test.go @@ -0,0 +1,69 @@ +package handlers_ec2_natgw + +import ( + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// --- recordToEC2 --- + +func TestRecordToEC2_Basic(t *testing.T) { + now := time.Now() + record := &NatGatewayRecord{ + NatGatewayId: "nat-abc", + VpcId: "vpc-1", + SubnetId: "subnet-1", + State: "available", + AccountID: "111222333444", + CreatedAt: now, + } + ngw := recordToEC2(record) + assert.Equal(t, "nat-abc", *ngw.NatGatewayId) + assert.Equal(t, "vpc-1", *ngw.VpcId) + assert.Equal(t, "subnet-1", *ngw.SubnetId) + assert.Equal(t, "available", *ngw.State) + assert.Equal(t, "public", *ngw.ConnectivityType) + assert.Equal(t, now, *ngw.CreateTime) + assert.Empty(t, ngw.NatGatewayAddresses, "no addresses when PublicIp empty") + assert.Empty(t, ngw.Tags) +} + +func TestRecordToEC2_WithPublicIp(t *testing.T) { + record := &NatGatewayRecord{ + NatGatewayId: "nat-1", + VpcId: "vpc-1", + SubnetId: "subnet-1", + AllocationId: "eipalloc-abc", + PublicIp: "54.1.2.3", + State: "available", + CreatedAt: time.Now(), + } + ngw := recordToEC2(record) + require.Len(t, ngw.NatGatewayAddresses, 1) + assert.Equal(t, "eipalloc-abc", *ngw.NatGatewayAddresses[0].AllocationId) + assert.Equal(t, "54.1.2.3", *ngw.NatGatewayAddresses[0].PublicIp) +} + +func TestRecordToEC2_WithTags(t *testing.T) { + record := &NatGatewayRecord{ + NatGatewayId: "nat-1", + VpcId: "vpc-1", + SubnetId: "subnet-1", + State: "available", + Tags: map[string]string{"Name": "my-nat", "env": "test"}, + CreatedAt: time.Now(), + } + ngw := recordToEC2(record) + require.Len(t, ngw.Tags, 2) + + tagMap := make(map[string]string) + for _, tag := range ngw.Tags { + tagMap[aws.StringValue(tag.Key)] = aws.StringValue(tag.Value) + } + assert.Equal(t, "my-nat", tagMap["Name"]) + assert.Equal(t, "test", tagMap["env"]) +} diff --git a/spinifex/handlers/ec2/routetable/helpers_test.go b/spinifex/handlers/ec2/routetable/helpers_test.go new file mode 100644 index 000000000..6fcc91dfd --- /dev/null +++ b/spinifex/handlers/ec2/routetable/helpers_test.go @@ -0,0 +1,245 @@ +package handlers_ec2_routetable + +import ( + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// --- containsString --- + +func TestContainsString_Found(t *testing.T) { + assert.True(t, containsString([]string{"a", "b", "c"}, "b")) +} + +func TestContainsString_NotFound(t *testing.T) { + assert.False(t, containsString([]string{"a", "b", "c"}, "d")) +} + +func TestContainsString_EmptySlice(t *testing.T) { + assert.False(t, containsString([]string{}, "a")) +} + +func TestContainsString_NilSlice(t *testing.T) { + assert.False(t, containsString(nil, "a")) +} + +// --- matchesFilters --- + +func TestMatchesFilters_EmptyFilters(t *testing.T) { + record := &RouteTableRecord{RouteTableId: "rtb-1", VpcId: "vpc-1"} + assert.True(t, matchesFilters(record, map[string][]string{})) +} + +func TestMatchesFilters_VpcId(t *testing.T) { + record := &RouteTableRecord{RouteTableId: "rtb-1", VpcId: "vpc-aaa"} + assert.True(t, matchesFilters(record, map[string][]string{"vpc-id": {"vpc-aaa"}})) + assert.False(t, matchesFilters(record, map[string][]string{"vpc-id": {"vpc-bbb"}})) +} + +func TestMatchesFilters_RouteTableId(t *testing.T) { + record := &RouteTableRecord{RouteTableId: "rtb-xyz", VpcId: "vpc-1"} + assert.True(t, matchesFilters(record, map[string][]string{"route-table-id": {"rtb-xyz"}})) + assert.False(t, matchesFilters(record, map[string][]string{"route-table-id": {"rtb-other"}})) +} + +func TestMatchesFilters_AssociationMain_True(t *testing.T) { + record := &RouteTableRecord{ + Associations: []AssociationRecord{ + {AssociationId: "rtbassoc-1", Main: true}, + }, + } + assert.True(t, matchesFilters(record, map[string][]string{"association.main": {"true"}})) + assert.False(t, matchesFilters(record, map[string][]string{"association.main": {"false"}})) +} + +func TestMatchesFilters_AssociationMain_False(t *testing.T) { + record := &RouteTableRecord{ + Associations: []AssociationRecord{ + {AssociationId: "rtbassoc-1", Main: false}, + }, + } + assert.False(t, matchesFilters(record, map[string][]string{"association.main": {"true"}})) +} + +func TestMatchesFilters_AssociationId(t *testing.T) { + record := &RouteTableRecord{ + Associations: []AssociationRecord{ + {AssociationId: "rtbassoc-abc"}, + }, + } + assert.True(t, matchesFilters(record, map[string][]string{ + "association.route-table-association-id": {"rtbassoc-abc"}, + })) + assert.False(t, matchesFilters(record, map[string][]string{ + "association.route-table-association-id": {"rtbassoc-other"}, + })) +} + +func TestMatchesFilters_AssociationSubnetId(t *testing.T) { + record := &RouteTableRecord{ + Associations: []AssociationRecord{ + {AssociationId: "rtbassoc-1", SubnetId: "subnet-aaa"}, + }, + } + assert.True(t, matchesFilters(record, map[string][]string{ + "association.subnet-id": {"subnet-aaa"}, + })) + assert.False(t, matchesFilters(record, map[string][]string{ + "association.subnet-id": {"subnet-bbb"}, + })) +} + +func TestMatchesFilters_RouteDestinationCidr(t *testing.T) { + record := &RouteTableRecord{ + Routes: []RouteRecord{ + {DestinationCidrBlock: "10.0.0.0/16", GatewayId: "local", State: "active"}, + {DestinationCidrBlock: "0.0.0.0/0", GatewayId: "igw-123", State: "active"}, + }, + } + assert.True(t, matchesFilters(record, map[string][]string{ + "route.destination-cidr-block": {"0.0.0.0/0"}, + })) + assert.False(t, matchesFilters(record, map[string][]string{ + "route.destination-cidr-block": {"192.168.0.0/16"}, + })) +} + +func TestMatchesFilters_RouteGatewayId(t *testing.T) { + record := &RouteTableRecord{ + Routes: []RouteRecord{ + {DestinationCidrBlock: "0.0.0.0/0", GatewayId: "igw-abc", State: "active"}, + }, + } + assert.True(t, matchesFilters(record, map[string][]string{ + "route.gateway-id": {"igw-abc"}, + })) + assert.False(t, matchesFilters(record, map[string][]string{ + "route.gateway-id": {"igw-other"}, + })) +} + +func TestMatchesFilters_MultipleFilters(t *testing.T) { + record := &RouteTableRecord{ + RouteTableId: "rtb-1", + VpcId: "vpc-aaa", + Routes: []RouteRecord{ + {DestinationCidrBlock: "0.0.0.0/0", GatewayId: "igw-123", State: "active"}, + }, + } + // Both match -> true + assert.True(t, matchesFilters(record, map[string][]string{ + "vpc-id": {"vpc-aaa"}, + "route.gateway-id": {"igw-123"}, + })) + // One mismatches -> false + assert.False(t, matchesFilters(record, map[string][]string{ + "vpc-id": {"vpc-aaa"}, + "route.gateway-id": {"igw-other"}, + })) +} + +func TestMatchesFilters_MultipleValues(t *testing.T) { + record := &RouteTableRecord{VpcId: "vpc-bbb"} + assert.True(t, matchesFilters(record, map[string][]string{ + "vpc-id": {"vpc-aaa", "vpc-bbb", "vpc-ccc"}, + })) +} + +func TestMatchesFilters_NoAssociations(t *testing.T) { + record := &RouteTableRecord{RouteTableId: "rtb-1"} + assert.False(t, matchesFilters(record, map[string][]string{ + "association.subnet-id": {"subnet-1"}, + })) +} + +func TestMatchesFilters_NoRoutes(t *testing.T) { + record := &RouteTableRecord{RouteTableId: "rtb-1"} + assert.False(t, matchesFilters(record, map[string][]string{ + "route.destination-cidr-block": {"0.0.0.0/0"}, + })) +} + +// --- recordToEC2 --- + +func TestRecordToEC2_Basic(t *testing.T) { + record := &RouteTableRecord{ + RouteTableId: "rtb-abc", + VpcId: "vpc-123", + AccountID: "111222333444", + } + rtb := recordToEC2(record) + assert.Equal(t, "rtb-abc", *rtb.RouteTableId) + assert.Equal(t, "vpc-123", *rtb.VpcId) + assert.Equal(t, "111222333444", *rtb.OwnerId) + assert.Empty(t, rtb.Routes) + assert.Empty(t, rtb.Associations) + assert.Empty(t, rtb.Tags) +} + +func TestRecordToEC2_Routes(t *testing.T) { + record := &RouteTableRecord{ + RouteTableId: "rtb-1", + VpcId: "vpc-1", + AccountID: "acct", + Routes: []RouteRecord{ + {DestinationCidrBlock: "10.0.0.0/16", GatewayId: "local", State: "active", Origin: "CreateRouteTable"}, + {DestinationCidrBlock: "0.0.0.0/0", GatewayId: "igw-123", State: "active", Origin: "CreateRoute"}, + {DestinationCidrBlock: "192.168.0.0/16", NatGatewayId: "nat-abc", State: "active", Origin: "CreateRoute"}, + }, + } + rtb := recordToEC2(record) + require.Len(t, rtb.Routes, 3) + + assert.Equal(t, "10.0.0.0/16", *rtb.Routes[0].DestinationCidrBlock) + assert.Equal(t, "local", *rtb.Routes[0].GatewayId) + assert.Nil(t, rtb.Routes[0].NatGatewayId) + + assert.Equal(t, "igw-123", *rtb.Routes[1].GatewayId) + + assert.Equal(t, "nat-abc", *rtb.Routes[2].NatGatewayId) + assert.Nil(t, rtb.Routes[2].GatewayId) +} + +func TestRecordToEC2_Associations(t *testing.T) { + record := &RouteTableRecord{ + RouteTableId: "rtb-1", + VpcId: "vpc-1", + AccountID: "acct", + Associations: []AssociationRecord{ + {AssociationId: "rtbassoc-main", Main: true}, + {AssociationId: "rtbassoc-sub", SubnetId: "subnet-aaa", Main: false}, + }, + } + rtb := recordToEC2(record) + require.Len(t, rtb.Associations, 2) + + assert.Equal(t, "rtbassoc-main", *rtb.Associations[0].RouteTableAssociationId) + assert.True(t, *rtb.Associations[0].Main) + assert.Nil(t, rtb.Associations[0].SubnetId) + assert.Equal(t, "associated", *rtb.Associations[0].AssociationState.State) + + assert.Equal(t, "rtbassoc-sub", *rtb.Associations[1].RouteTableAssociationId) + assert.False(t, *rtb.Associations[1].Main) + assert.Equal(t, "subnet-aaa", *rtb.Associations[1].SubnetId) +} + +func TestRecordToEC2_Tags(t *testing.T) { + record := &RouteTableRecord{ + RouteTableId: "rtb-1", + VpcId: "vpc-1", + AccountID: "acct", + Tags: map[string]string{"Name": "my-rt", "env": "prod"}, + } + rtb := recordToEC2(record) + require.Len(t, rtb.Tags, 2) + + tagMap := make(map[string]string) + for _, tag := range rtb.Tags { + tagMap[aws.StringValue(tag.Key)] = aws.StringValue(tag.Value) + } + assert.Equal(t, "my-rt", tagMap["Name"]) + assert.Equal(t, "prod", tagMap["env"]) +} diff --git a/spinifex/handlers/ec2/vpc/eni_mac_test.go b/spinifex/handlers/ec2/vpc/eni_mac_test.go new file mode 100644 index 000000000..8f8ac9da3 --- /dev/null +++ b/spinifex/handlers/ec2/vpc/eni_mac_test.go @@ -0,0 +1,32 @@ +package handlers_ec2_vpc + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGenerateENIMac_Deterministic(t *testing.T) { + mac1 := generateENIMac("eni-abc123") + mac2 := generateENIMac("eni-abc123") + assert.Equal(t, mac1, mac2, "same input must produce same MAC") +} + +func TestGenerateENIMac_DifferentInputs(t *testing.T) { + mac1 := generateENIMac("eni-aaa") + mac2 := generateENIMac("eni-bbb") + assert.NotEqual(t, mac1, mac2, "different inputs should produce different MACs") +} + +func TestGenerateENIMac_LocallyAdministered(t *testing.T) { + mac := generateENIMac("eni-test123") + // Must start with 02:00:00 (locally-administered unicast) + assert.Regexp(t, regexp.MustCompile(`^02:00:00:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}$`), mac) +} + +func TestGenerateENIMac_EmptyString(t *testing.T) { + mac := generateENIMac("") + // Hash of empty string = 0, so all three octets should be 00 + assert.Equal(t, "02:00:00:00:00:00", mac) +} diff --git a/spinifex/handlers/elbv2/arn_test.go b/spinifex/handlers/elbv2/arn_test.go new file mode 100644 index 000000000..d56162939 --- /dev/null +++ b/spinifex/handlers/elbv2/arn_test.go @@ -0,0 +1,36 @@ +package handlers_elbv2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBuildLBArn(t *testing.T) { + arn := buildLBArn("us-east-1", "123456789012", "my-alb", "50dc6c495c0c9188") + assert.Equal(t, "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/50dc6c495c0c9188", arn) +} + +func TestBuildTGArn(t *testing.T) { + arn := buildTGArn("us-west-2", "111222333444", "my-tg", "deadbeef") + assert.Equal(t, "arn:aws:elasticloadbalancing:us-west-2:111222333444:targetgroup/my-tg/deadbeef", arn) +} + +func TestBuildListenerArn(t *testing.T) { + arn := buildListenerArn("eu-west-1", "999888777666", "my-alb", "lbid123", "listener456") + assert.Equal(t, "arn:aws:elasticloadbalancing:eu-west-1:999888777666:listener/app/my-alb/lbid123/listener456", arn) +} + +func TestAlbVMUserData_DefaultNatsURL(t *testing.T) { + ud := albVMUserData("lb-abc123", "") + assert.Contains(t, ud, "#cloud-config") + assert.Contains(t, ud, "--lb-id=lb-abc123") + assert.Contains(t, ud, "--nats=nats://127.0.0.1:4222") +} + +func TestAlbVMUserData_CustomNatsURL(t *testing.T) { + ud := albVMUserData("lb-xyz", "nats://10.0.0.5:4222") + assert.Contains(t, ud, "--lb-id=lb-xyz") + assert.Contains(t, ud, "--nats=nats://10.0.0.5:4222") + assert.NotContains(t, ud, "127.0.0.1") +} diff --git a/spinifex/handlers/elbv2/coverage_test.go b/spinifex/handlers/elbv2/coverage_test.go new file mode 100644 index 000000000..8d9bb8773 --- /dev/null +++ b/spinifex/handlers/elbv2/coverage_test.go @@ -0,0 +1,297 @@ +package handlers_elbv2 + +import ( + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elbv2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// --- CreateTargetGroup: all health check fields + target type + tags --- + +func TestCreateTargetGroup_AllHealthCheckFields(t *testing.T) { + svc := setupTestService(t) + + out, err := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{ + Name: aws.String("full-hc"), + Port: aws.Int64(8080), + Protocol: aws.String("HTTPS"), + VpcId: aws.String("vpc-123"), + TargetType: aws.String("ip"), + HealthCheckProtocol: aws.String("HTTPS"), + HealthCheckPort: aws.String("8443"), + HealthCheckPath: aws.String("/health"), + HealthCheckIntervalSeconds: aws.Int64(15), + HealthCheckTimeoutSeconds: aws.Int64(5), + HealthyThresholdCount: aws.Int64(3), + UnhealthyThresholdCount: aws.Int64(2), + Matcher: &elbv2.Matcher{HttpCode: aws.String("200")}, + Tags: []*elbv2.Tag{ + {Key: aws.String("env"), Value: aws.String("prod")}, + {Key: aws.String("team"), Value: aws.String("platform")}, + }, + }, testAccountID) + + require.NoError(t, err) + tg := out.TargetGroups[0] + assert.Equal(t, "ip", *tg.TargetType) + assert.Equal(t, "HTTPS", *tg.HealthCheckProtocol) + assert.Equal(t, "8443", *tg.HealthCheckPort) + assert.Equal(t, "/health", *tg.HealthCheckPath) + assert.Equal(t, int64(15), *tg.HealthCheckIntervalSeconds) + assert.Equal(t, int64(5), *tg.HealthCheckTimeoutSeconds) + assert.Equal(t, int64(3), *tg.HealthyThresholdCount) + assert.Equal(t, int64(2), *tg.UnhealthyThresholdCount) + assert.Equal(t, "200", *tg.Matcher.HttpCode) + assert.Equal(t, int64(8080), *tg.Port) + assert.Equal(t, "vpc-123", *tg.VpcId) +} + +// --- DescribeLoadBalancers: ARN filter --- + +func TestDescribeLoadBalancers_FilterByArn(t *testing.T) { + svc := setupTestService(t) + + out1, _ := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("lb-arn-a")}, testAccountID) + _, _ = svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("lb-arn-b")}, testAccountID) + + desc, err := svc.DescribeLoadBalancers(&elbv2.DescribeLoadBalancersInput{ + LoadBalancerArns: []*string{out1.LoadBalancers[0].LoadBalancerArn}, + }, testAccountID) + require.NoError(t, err) + require.Len(t, desc.LoadBalancers, 1) + assert.Equal(t, "lb-arn-a", *desc.LoadBalancers[0].LoadBalancerName) +} + +// --- DescribeTargetGroups: by name --- + +func TestDescribeTargetGroups_FilterByName(t *testing.T) { + svc := setupTestService(t) + + _, _ = svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-alpha")}, testAccountID) + _, _ = svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-beta")}, testAccountID) + + desc, err := svc.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{ + Names: []*string{aws.String("tg-alpha")}, + }, testAccountID) + require.NoError(t, err) + require.Len(t, desc.TargetGroups, 1) + assert.Equal(t, "tg-alpha", *desc.TargetGroups[0].TargetGroupName) +} + +// --- DescribeTargetGroups: by ARN --- + +func TestDescribeTargetGroups_FilterByArn(t *testing.T) { + svc := setupTestService(t) + + out1, _ := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-arn-a")}, testAccountID) + _, _ = svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-arn-b")}, testAccountID) + + desc, err := svc.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{ + TargetGroupArns: []*string{out1.TargetGroups[0].TargetGroupArn}, + }, testAccountID) + require.NoError(t, err) + require.Len(t, desc.TargetGroups, 1) + assert.Equal(t, "tg-arn-a", *desc.TargetGroups[0].TargetGroupName) +} + +// --- DescribeListeners: filter by listener ARN --- + +func TestDescribeListeners_FilterByListenerArn(t *testing.T) { + svc := setupTestService(t) + + lb, _ := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("lb-larn")}, testAccountID) + tg, _ := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-larn")}, testAccountID) + + l1, _ := svc.CreateListener(&elbv2.CreateListenerInput{ + LoadBalancerArn: lb.LoadBalancers[0].LoadBalancerArn, + Port: aws.Int64(80), + DefaultActions: []*elbv2.Action{{Type: aws.String("forward"), TargetGroupArn: tg.TargetGroups[0].TargetGroupArn}}, + }, testAccountID) + _, _ = svc.CreateListener(&elbv2.CreateListenerInput{ + LoadBalancerArn: lb.LoadBalancers[0].LoadBalancerArn, + Port: aws.Int64(443), + DefaultActions: []*elbv2.Action{{Type: aws.String("forward"), TargetGroupArn: tg.TargetGroups[0].TargetGroupArn}}, + }, testAccountID) + + desc, err := svc.DescribeListeners(&elbv2.DescribeListenersInput{ + ListenerArns: []*string{l1.Listeners[0].ListenerArn}, + }, testAccountID) + require.NoError(t, err) + require.Len(t, desc.Listeners, 1) + assert.Equal(t, int64(80), *desc.Listeners[0].Port) +} + +// --- RegisterTargets: nil target id skipped --- + +func TestRegisterTargets_NilTargetSkipped(t *testing.T) { + svc := setupTestService(t) + + tg, _ := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-nil"), Port: aws.Int64(80)}, testAccountID) + + _, err := svc.RegisterTargets(&elbv2.RegisterTargetsInput{ + TargetGroupArn: tg.TargetGroups[0].TargetGroupArn, + Targets: []*elbv2.TargetDescription{ + {Id: nil}, + {Id: aws.String("i-valid")}, + }, + }, testAccountID) + require.NoError(t, err) + + health, _ := svc.DescribeTargetHealth(&elbv2.DescribeTargetHealthInput{ + TargetGroupArn: tg.TargetGroups[0].TargetGroupArn, + }, testAccountID) + require.Len(t, health.TargetHealthDescriptions, 1) + assert.Equal(t, "i-valid", *health.TargetHealthDescriptions[0].Target.Id) +} + +// --- RegisterTargets / DeregisterTargets: missing ARN --- + +func TestRegisterTargets_MissingArn(t *testing.T) { + svc := setupTestService(t) + _, err := svc.RegisterTargets(&elbv2.RegisterTargetsInput{}, testAccountID) + assert.Error(t, err) +} + +func TestDeregisterTargets_MissingArn(t *testing.T) { + svc := setupTestService(t) + _, err := svc.DeregisterTargets(&elbv2.DeregisterTargetsInput{}, testAccountID) + assert.Error(t, err) +} + +func TestDeregisterTargets_TGNotFound(t *testing.T) { + svc := setupTestService(t) + _, err := svc.DeregisterTargets(&elbv2.DeregisterTargetsInput{ + TargetGroupArn: aws.String("arn:nonexistent"), + }, testAccountID) + assert.Error(t, err) +} + +// --- CreateListener: custom protocol --- + +func TestCreateListener_CustomProtocol(t *testing.T) { + svc := setupTestService(t) + + lb, _ := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("lb-proto")}, testAccountID) + tg, _ := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-proto")}, testAccountID) + + out, err := svc.CreateListener(&elbv2.CreateListenerInput{ + LoadBalancerArn: lb.LoadBalancers[0].LoadBalancerArn, + Protocol: aws.String("HTTPS"), + Port: aws.Int64(443), + DefaultActions: []*elbv2.Action{{Type: aws.String("forward"), TargetGroupArn: tg.TargetGroups[0].TargetGroupArn}}, + }, testAccountID) + require.NoError(t, err) + assert.Equal(t, "HTTPS", *out.Listeners[0].Protocol) + assert.Equal(t, int64(443), *out.Listeners[0].Port) +} + +// --- CreateListener: missing actions --- + +func TestCreateListener_MissingActions(t *testing.T) { + svc := setupTestService(t) + lb, _ := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("lb-noact")}, testAccountID) + + _, err := svc.CreateListener(&elbv2.CreateListenerInput{ + LoadBalancerArn: lb.LoadBalancers[0].LoadBalancerArn, + }, testAccountID) + assert.Error(t, err) +} + +// --- DeleteTargetGroup: missing ARN --- + +func TestDeleteTargetGroup_MissingArn(t *testing.T) { + svc := setupTestService(t) + _, err := svc.DeleteTargetGroup(&elbv2.DeleteTargetGroupInput{}, testAccountID) + assert.Error(t, err) +} + +// --- DeleteListener: missing ARN --- + +func TestDeleteListener_MissingArn(t *testing.T) { + svc := setupTestService(t) + _, err := svc.DeleteListener(&elbv2.DeleteListenerInput{}, testAccountID) + assert.Error(t, err) +} + +// --- DescribeTargetHealth: missing ARN --- + +func TestDescribeTargetHealth_MissingArn(t *testing.T) { + svc := setupTestService(t) + _, err := svc.DescribeTargetHealth(&elbv2.DescribeTargetHealthInput{}, testAccountID) + assert.Error(t, err) +} + +// --- DescribeLoadBalancers: empty filters returns all --- + +func TestDescribeLoadBalancers_All(t *testing.T) { + svc := setupTestService(t) + + _, _ = svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("lb-all-a")}, testAccountID) + _, _ = svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("lb-all-b")}, testAccountID) + + desc, err := svc.DescribeLoadBalancers(&elbv2.DescribeLoadBalancersInput{}, testAccountID) + require.NoError(t, err) + assert.GreaterOrEqual(t, len(desc.LoadBalancers), 2) +} + +// --- DescribeTargetGroups: all --- + +func TestDescribeTargetGroups_All(t *testing.T) { + svc := setupTestService(t) + + _, _ = svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-all-a")}, testAccountID) + _, _ = svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-all-b")}, testAccountID) + + desc, err := svc.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{}, testAccountID) + require.NoError(t, err) + assert.GreaterOrEqual(t, len(desc.TargetGroups), 2) +} + +// --- DescribeListeners: all (no LB ARN filter) --- + +func TestDescribeListeners_All(t *testing.T) { + svc := setupTestService(t) + + lb, _ := svc.CreateLoadBalancer(&elbv2.CreateLoadBalancerInput{Name: aws.String("lb-lall")}, testAccountID) + tg, _ := svc.CreateTargetGroup(&elbv2.CreateTargetGroupInput{Name: aws.String("tg-lall")}, testAccountID) + _, _ = svc.CreateListener(&elbv2.CreateListenerInput{ + LoadBalancerArn: lb.LoadBalancers[0].LoadBalancerArn, + DefaultActions: []*elbv2.Action{{Type: aws.String("forward"), TargetGroupArn: tg.TargetGroups[0].TargetGroupArn}}, + }, testAccountID) + + desc, err := svc.DescribeListeners(&elbv2.DescribeListenersInput{}, testAccountID) + require.NoError(t, err) + assert.GreaterOrEqual(t, len(desc.Listeners), 1) +} + +// --- DeleteLoadBalancer: missing ARN --- + +func TestDeleteLoadBalancer_MissingArn(t *testing.T) { + svc := setupTestService(t) + _, err := svc.DeleteLoadBalancer(&elbv2.DeleteLoadBalancerInput{}, testAccountID) + assert.Error(t, err) +} + +// --- RegisterTargets: TG not found --- + +func TestRegisterTargets_TGNotFound(t *testing.T) { + svc := setupTestService(t) + _, err := svc.RegisterTargets(&elbv2.RegisterTargetsInput{ + TargetGroupArn: aws.String("arn:nonexistent"), + Targets: []*elbv2.TargetDescription{{Id: aws.String("i-abc")}}, + }, testAccountID) + assert.Error(t, err) +} + +// --- CreateListener: missing LB ARN --- + +func TestCreateListener_MissingLBArn(t *testing.T) { + svc := setupTestService(t) + _, err := svc.CreateListener(&elbv2.CreateListenerInput{ + DefaultActions: []*elbv2.Action{{Type: aws.String("forward")}}, + }, testAccountID) + assert.Error(t, err) +} diff --git a/spinifex/services/vpcd/helpers_test.go b/spinifex/services/vpcd/helpers_test.go new file mode 100644 index 000000000..8a887812a --- /dev/null +++ b/spinifex/services/vpcd/helpers_test.go @@ -0,0 +1,77 @@ +package vpcd + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Additional edge-case tests for subnetGateway and generateMAC. +// Core happy-path tests are in topology_test.go. + +func TestSubnetGateway_Slash28(t *testing.T) { + gw, prefix, err := subnetGateway("172.31.0.0/28") + require.NoError(t, err) + assert.Equal(t, "172.31.0.1", gw) + assert.Equal(t, 28, prefix) +} + +func TestSubnetGateway_IPv6(t *testing.T) { + _, _, err := subnetGateway("2001:db8::/32") + require.Error(t, err) + assert.Contains(t, err.Error(), "only IPv4 supported") +} + +func TestGenerateMAC_EmptyString(t *testing.T) { + mac := generateMAC("") + assert.Equal(t, "02:00:00:00:00:00", mac) +} + +// --- dnsServer --- + +func TestDnsServer_WithDNSServers(t *testing.T) { + pools := []ExternalPoolConfig{ + {Name: "test", DNSServers: []string{"10.0.0.2", "10.0.0.3"}}, + } + h := NewTopologyHandler(nil, WithExternalNetwork("pool", pools)) + assert.Equal(t, "{10.0.0.2, 10.0.0.3}", h.dnsServer()) +} + +func TestDnsServer_SingleDNS(t *testing.T) { + pools := []ExternalPoolConfig{ + {Name: "test", DNSServers: []string{"1.2.3.4"}}, + } + h := NewTopologyHandler(nil, WithExternalNetwork("pool", pools)) + assert.Equal(t, "{1.2.3.4}", h.dnsServer()) +} + +func TestDnsServer_NoPools(t *testing.T) { + h := NewTopologyHandler(nil) + assert.Equal(t, "{8.8.8.8, 1.1.1.1}", h.dnsServer()) +} + +func TestDnsServer_PoolWithNoDNS(t *testing.T) { + pools := []ExternalPoolConfig{ + {Name: "test"}, + } + h := NewTopologyHandler(nil, WithExternalNetwork("pool", pools)) + assert.Equal(t, "{8.8.8.8, 1.1.1.1}", h.dnsServer()) +} + +// --- isMacvlanMode --- + +func TestIsMacvlanMode_Default(t *testing.T) { + h := NewTopologyHandler(nil) + assert.True(t, h.isMacvlanMode(), "default (empty) should be macvlan") +} + +func TestIsMacvlanMode_Direct(t *testing.T) { + h := NewTopologyHandler(nil, WithBridgeMode(BridgeModeDirect)) + assert.False(t, h.isMacvlanMode()) +} + +func TestIsMacvlanMode_Macvlan(t *testing.T) { + h := NewTopologyHandler(nil, WithBridgeMode(BridgeModeMacvlan)) + assert.True(t, h.isMacvlanMode()) +}