From e53e7a58841805112c7a8f523bc1d70fd7047ac8 Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:12:17 +1000 Subject: [PATCH] test: cover subscriber topology handler happy paths Drives every vpc.* topology topic end to end against the mock OVN client and asserts the resulting rows: router, subnet switch and router port, logical switch port addresses and port-group membership, gateway router port, NAT rules and egress policies. Invalid CIDR, IP and MAC events are pinned to a failed reply envelope with no partial write. Moves the elbv2 dispatch table to an external test package. subscribers 53.5% -> 73.4%. --- spinifex/gateway/elbv2/dispatch_test.go | 165 +++---- spinifex/network/subscribers/topology_test.go | 403 ++++++++++++++++++ 2 files changed, 487 insertions(+), 81 deletions(-) create mode 100644 spinifex/network/subscribers/topology_test.go diff --git a/spinifex/gateway/elbv2/dispatch_test.go b/spinifex/gateway/elbv2/dispatch_test.go index 4b663782f..ee3c5070c 100644 --- a/spinifex/gateway/elbv2/dispatch_test.go +++ b/spinifex/gateway/elbv2/dispatch_test.go @@ -1,4 +1,4 @@ -package gateway_elbv2 +package gateway_elbv2_test import ( "context" @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/elbv2" "github.com/mulgadc/spinifex/spinifex/awserrors" + gateway_elbv2 "github.com/mulgadc/spinifex/spinifex/gateway/elbv2" handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2" "github.com/mulgadc/spinifex/spinifex/testutil" "github.com/mulgadc/spinifex/spinifex/utils" @@ -22,6 +23,8 @@ const ( testLBArn = "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/abc" testTGArn = "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/my-tg/abc" testRuleArn = "arn:aws:elasticloadbalancing:us-east-1:123456789012:listener-rule/app/my-alb/abc/def/ghi" + testLsnrArn = "arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/lb/abc/def" + testCrtArn = "arn:aws:acm:us-east-1:123456789012:certificate/aaaa-1111" ) // dispatchCase pins one gateway wrapper to the NATS subject it must publish on, @@ -73,25 +76,25 @@ func dispatchCases() []dispatchCase { createListenerIn := &elbv2.CreateListenerInput{ LoadBalancerArn: aws.String(testLBArn), Port: aws.Int64(443), DefaultActions: actions, } - createListenerOut := elbv2.CreateListenerOutput{Listeners: []*elbv2.Listener{{ListenerArn: aws.String(testListenerArn)}}} - delListenerIn := &elbv2.DeleteListenerInput{ListenerArn: aws.String(testListenerArn)} - modListenerIn := &elbv2.ModifyListenerInput{ListenerArn: aws.String(testListenerArn), Port: aws.Int64(8443)} + createListenerOut := elbv2.CreateListenerOutput{Listeners: []*elbv2.Listener{{ListenerArn: aws.String(testLsnrArn)}}} + delListenerIn := &elbv2.DeleteListenerInput{ListenerArn: aws.String(testLsnrArn)} + modListenerIn := &elbv2.ModifyListenerInput{ListenerArn: aws.String(testLsnrArn), Port: aws.Int64(8443)} modListenerOut := elbv2.ModifyListenerOutput{Listeners: []*elbv2.Listener{{ - ListenerArn: aws.String(testListenerArn), Port: aws.Int64(8443), + ListenerArn: aws.String(testLsnrArn), Port: aws.Int64(8443), }}} descListenersIn := &elbv2.DescribeListenersInput{LoadBalancerArn: aws.String(testLBArn)} - descListenersOut := elbv2.DescribeListenersOutput{Listeners: []*elbv2.Listener{{ListenerArn: aws.String(testListenerArn)}}} + descListenersOut := elbv2.DescribeListenersOutput{Listeners: []*elbv2.Listener{{ListenerArn: aws.String(testLsnrArn)}}} conditions := []*elbv2.RuleCondition{{Field: aws.String("path-pattern"), Values: []*string{aws.String("/api/*")}}} createRuleIn := &elbv2.CreateRuleInput{ - ListenerArn: aws.String(testListenerArn), Priority: aws.Int64(10), + ListenerArn: aws.String(testLsnrArn), Priority: aws.Int64(10), Conditions: conditions, Actions: actions, } createRuleOut := elbv2.CreateRuleOutput{Rules: []*elbv2.Rule{{RuleArn: aws.String(testRuleArn), Priority: aws.String("10")}}} modRuleIn := &elbv2.ModifyRuleInput{RuleArn: aws.String(testRuleArn), Conditions: conditions} modRuleOut := elbv2.ModifyRuleOutput{Rules: []*elbv2.Rule{{RuleArn: aws.String(testRuleArn)}}} delRuleIn := &elbv2.DeleteRuleInput{RuleArn: aws.String(testRuleArn)} - descRulesIn := &elbv2.DescribeRulesInput{ListenerArn: aws.String(testListenerArn)} + descRulesIn := &elbv2.DescribeRulesInput{ListenerArn: aws.String(testLsnrArn)} descRulesOut := elbv2.DescribeRulesOutput{Rules: []*elbv2.Rule{{RuleArn: aws.String(testRuleArn)}}} prioIn := &elbv2.SetRulePrioritiesInput{RulePriorities: []*elbv2.RulePriorityPair{{ RuleArn: aws.String(testRuleArn), Priority: aws.Int64(5), @@ -127,11 +130,11 @@ func dispatchCases() []dispatchCase { subnetsIn := &elbv2.SetSubnetsInput{LoadBalancerArn: aws.String(testLBArn), Subnets: []*string{aws.String("subnet-1")}} subnetsOut := elbv2.SetSubnetsOutput{AvailabilityZones: []*elbv2.AvailabilityZone{{SubnetId: aws.String("subnet-1")}}} - certs := []*elbv2.Certificate{{CertificateArn: aws.String(testCertArn)}} - addCertIn := &elbv2.AddListenerCertificatesInput{ListenerArn: aws.String(testListenerArn), Certificates: certs} + certs := []*elbv2.Certificate{{CertificateArn: aws.String(testCrtArn)}} + addCertIn := &elbv2.AddListenerCertificatesInput{ListenerArn: aws.String(testLsnrArn), Certificates: certs} addCertOut := elbv2.AddListenerCertificatesOutput{Certificates: certs} - removeCertIn := &elbv2.RemoveListenerCertificatesInput{ListenerArn: aws.String(testListenerArn), Certificates: certs} - descCertIn := &elbv2.DescribeListenerCertificatesInput{ListenerArn: aws.String(testListenerArn)} + removeCertIn := &elbv2.RemoveListenerCertificatesInput{ListenerArn: aws.String(testLsnrArn), Certificates: certs} + descCertIn := &elbv2.DescribeListenerCertificatesInput{ListenerArn: aws.String(testLsnrArn)} descCertOut := elbv2.DescribeListenerCertificatesOutput{Certificates: certs} sslIn := &elbv2.DescribeSSLPoliciesInput{Names: []*string{aws.String("ELBSecurityPolicy-2016-08")}} sslOut := elbv2.DescribeSSLPoliciesOutput{SslPolicies: []*elbv2.SslPolicy{{Name: aws.String("ELBSecurityPolicy-2016-08")}}} @@ -153,210 +156,210 @@ func dispatchCases() []dispatchCase { name: "CreateLoadBalancer", subject: "elbv2.CreateLoadBalancer", input: lbIn, reply: lbOut, want: lbOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return CreateLoadBalancer(ctx, lbIn, nc, testAccountID) + return gateway_elbv2.CreateLoadBalancer(ctx, lbIn, nc, testAccountID) }, }, { name: "DeleteLoadBalancer", subject: "elbv2.DeleteLoadBalancer", input: delLBIn, reply: elbv2.DeleteLoadBalancerOutput{}, want: elbv2.DeleteLoadBalancerOutput{}, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return DeleteLoadBalancer(ctx, delLBIn, nc, testAccountID) + return gateway_elbv2.DeleteLoadBalancer(ctx, delLBIn, nc, testAccountID) }, }, { name: "DescribeLoadBalancers", subject: "elbv2.DescribeLoadBalancers", input: descLBIn, reply: descLBOut, want: descLBOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return DescribeLoadBalancers(ctx, descLBIn, nc, testAccountID) + return gateway_elbv2.DescribeLoadBalancers(ctx, descLBIn, nc, testAccountID) }, }, { name: "CreateTargetGroup", subject: "elbv2.CreateTargetGroup", input: tgIn, reply: tgOut, want: tgOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return CreateTargetGroup(ctx, tgIn, nc, testAccountID) + return gateway_elbv2.CreateTargetGroup(ctx, tgIn, nc, testAccountID) }, }, { name: "ModifyTargetGroup", subject: "elbv2.ModifyTargetGroup", input: modTGIn, reply: modTGOut, want: modTGOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return ModifyTargetGroup(ctx, modTGIn, nc, testAccountID) + return gateway_elbv2.ModifyTargetGroup(ctx, modTGIn, nc, testAccountID) }, }, { name: "DeleteTargetGroup", subject: "elbv2.DeleteTargetGroup", input: delTGIn, reply: elbv2.DeleteTargetGroupOutput{}, want: elbv2.DeleteTargetGroupOutput{}, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return DeleteTargetGroup(ctx, delTGIn, nc, testAccountID) + return gateway_elbv2.DeleteTargetGroup(ctx, delTGIn, nc, testAccountID) }, }, { name: "DescribeTargetGroups", subject: "elbv2.DescribeTargetGroups", input: descTGIn, reply: descTGOut, want: descTGOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return DescribeTargetGroups(ctx, descTGIn, nc, testAccountID) + return gateway_elbv2.DescribeTargetGroups(ctx, descTGIn, nc, testAccountID) }, }, { name: "RegisterTargets", subject: "elbv2.RegisterTargets", input: regIn, reply: elbv2.RegisterTargetsOutput{}, want: elbv2.RegisterTargetsOutput{}, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return RegisterTargets(ctx, regIn, nc, testAccountID) + return gateway_elbv2.RegisterTargets(ctx, regIn, nc, testAccountID) }, }, { name: "DeregisterTargets", subject: "elbv2.DeregisterTargets", input: deregIn, reply: elbv2.DeregisterTargetsOutput{}, want: elbv2.DeregisterTargetsOutput{}, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return DeregisterTargets(ctx, deregIn, nc, testAccountID) + return gateway_elbv2.DeregisterTargets(ctx, deregIn, nc, testAccountID) }, }, { name: "DescribeTargetHealth", subject: "elbv2.DescribeTargetHealth", input: healthIn, reply: healthOut, want: healthOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return DescribeTargetHealth(ctx, healthIn, nc, testAccountID) + return gateway_elbv2.DescribeTargetHealth(ctx, healthIn, nc, testAccountID) }, }, { name: "CreateListener", subject: "elbv2.CreateListener", input: createListenerIn, reply: createListenerOut, want: createListenerOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return CreateListener(ctx, createListenerIn, nc, testAccountID) + return gateway_elbv2.CreateListener(ctx, createListenerIn, nc, testAccountID) }, }, { name: "DeleteListener", subject: "elbv2.DeleteListener", input: delListenerIn, reply: elbv2.DeleteListenerOutput{}, want: elbv2.DeleteListenerOutput{}, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return DeleteListener(ctx, delListenerIn, nc, testAccountID) + return gateway_elbv2.DeleteListener(ctx, delListenerIn, nc, testAccountID) }, }, { name: "ModifyListener", subject: "elbv2.ModifyListener", input: modListenerIn, reply: modListenerOut, want: modListenerOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return ModifyListener(ctx, modListenerIn, nc, testAccountID) + return gateway_elbv2.ModifyListener(ctx, modListenerIn, nc, testAccountID) }, }, { name: "DescribeListeners", subject: "elbv2.DescribeListeners", input: descListenersIn, reply: descListenersOut, want: descListenersOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return DescribeListeners(ctx, descListenersIn, nc, testAccountID) + return gateway_elbv2.DescribeListeners(ctx, descListenersIn, nc, testAccountID) }, }, { name: "CreateRule", subject: "elbv2.CreateRule", input: createRuleIn, reply: createRuleOut, want: createRuleOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return CreateRule(ctx, createRuleIn, nc, testAccountID) + return gateway_elbv2.CreateRule(ctx, createRuleIn, nc, testAccountID) }, }, { name: "ModifyRule", subject: "elbv2.ModifyRule", input: modRuleIn, reply: modRuleOut, want: modRuleOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return ModifyRule(ctx, modRuleIn, nc, testAccountID) + return gateway_elbv2.ModifyRule(ctx, modRuleIn, nc, testAccountID) }, }, { name: "DeleteRule", subject: "elbv2.DeleteRule", input: delRuleIn, reply: elbv2.DeleteRuleOutput{}, want: elbv2.DeleteRuleOutput{}, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return DeleteRule(ctx, delRuleIn, nc, testAccountID) + return gateway_elbv2.DeleteRule(ctx, delRuleIn, nc, testAccountID) }, }, { name: "DescribeRules", subject: "elbv2.DescribeRules", input: descRulesIn, reply: descRulesOut, want: descRulesOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return DescribeRules(ctx, descRulesIn, nc, testAccountID) + return gateway_elbv2.DescribeRules(ctx, descRulesIn, nc, testAccountID) }, }, { name: "SetRulePriorities", subject: "elbv2.SetRulePriorities", input: prioIn, reply: prioOut, want: prioOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return SetRulePriorities(ctx, prioIn, nc, testAccountID) + return gateway_elbv2.SetRulePriorities(ctx, prioIn, nc, testAccountID) }, }, { name: "DescribeTags", subject: "elbv2.DescribeTags", input: descTagsIn, reply: descTagsOut, want: descTagsOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return DescribeTags(ctx, descTagsIn, nc, testAccountID) + return gateway_elbv2.DescribeTags(ctx, descTagsIn, nc, testAccountID) }, }, { name: "AddTags", subject: "elbv2.AddTags", input: addTagsIn, reply: elbv2.AddTagsOutput{}, want: elbv2.AddTagsOutput{}, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return AddTags(ctx, addTagsIn, nc, testAccountID) + return gateway_elbv2.AddTags(ctx, addTagsIn, nc, testAccountID) }, }, { name: "RemoveTags", subject: "elbv2.RemoveTags", input: removeTagsIn, reply: elbv2.RemoveTagsOutput{}, want: elbv2.RemoveTagsOutput{}, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return RemoveTags(ctx, removeTagsIn, nc, testAccountID) + return gateway_elbv2.RemoveTags(ctx, removeTagsIn, nc, testAccountID) }, }, { name: "ModifyTargetGroupAttributes", subject: "elbv2.ModifyTargetGroupAttributes", input: modTGAttrIn, reply: modTGAttrOut, want: modTGAttrOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return ModifyTargetGroupAttributes(ctx, modTGAttrIn, nc, testAccountID) + return gateway_elbv2.ModifyTargetGroupAttributes(ctx, modTGAttrIn, nc, testAccountID) }, }, { name: "DescribeTargetGroupAttributes", subject: "elbv2.DescribeTargetGroupAttributes", input: descTGAttrIn, reply: descTGAttrOut, want: descTGAttrOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return DescribeTargetGroupAttributes(ctx, descTGAttrIn, nc, testAccountID) + return gateway_elbv2.DescribeTargetGroupAttributes(ctx, descTGAttrIn, nc, testAccountID) }, }, { name: "ModifyLoadBalancerAttributes", subject: "elbv2.ModifyLoadBalancerAttributes", input: modLBAttrIn, reply: modLBAttrOut, want: modLBAttrOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return ModifyLoadBalancerAttributes(ctx, modLBAttrIn, nc, testAccountID) + return gateway_elbv2.ModifyLoadBalancerAttributes(ctx, modLBAttrIn, nc, testAccountID) }, }, { name: "DescribeLoadBalancerAttributes", subject: "elbv2.DescribeLoadBalancerAttributes", input: descLBAttrIn, reply: descLBAttrOut, want: descLBAttrOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return DescribeLoadBalancerAttributes(ctx, descLBAttrIn, nc, testAccountID) + return gateway_elbv2.DescribeLoadBalancerAttributes(ctx, descLBAttrIn, nc, testAccountID) }, }, { name: "SetSecurityGroups", subject: "elbv2.SetSecurityGroups", input: sgIn, reply: sgOut, want: sgOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return SetSecurityGroups(ctx, sgIn, nc, testAccountID) + return gateway_elbv2.SetSecurityGroups(ctx, sgIn, nc, testAccountID) }, }, { name: "SetIpAddressType", subject: "elbv2.SetIpAddressType", input: ipTypeIn, reply: ipTypeOut, want: ipTypeOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return SetIpAddressType(ctx, ipTypeIn, nc, testAccountID) + return gateway_elbv2.SetIpAddressType(ctx, ipTypeIn, nc, testAccountID) }, }, { name: "SetSubnets", subject: "elbv2.SetSubnets", input: subnetsIn, reply: subnetsOut, want: subnetsOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return SetSubnets(ctx, subnetsIn, nc, testAccountID) + return gateway_elbv2.SetSubnets(ctx, subnetsIn, nc, testAccountID) }, }, { name: "AddListenerCertificates", subject: "elbv2.AddListenerCertificates", input: addCertIn, reply: addCertOut, want: addCertOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return AddListenerCertificates(ctx, addCertIn, nc, testAccountID) + return gateway_elbv2.AddListenerCertificates(ctx, addCertIn, nc, testAccountID) }, }, { @@ -364,35 +367,35 @@ func dispatchCases() []dispatchCase { input: removeCertIn, reply: elbv2.RemoveListenerCertificatesOutput{}, want: elbv2.RemoveListenerCertificatesOutput{}, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return RemoveListenerCertificates(ctx, removeCertIn, nc, testAccountID) + return gateway_elbv2.RemoveListenerCertificates(ctx, removeCertIn, nc, testAccountID) }, }, { name: "DescribeListenerCertificates", subject: "elbv2.DescribeListenerCertificates", input: descCertIn, reply: descCertOut, want: descCertOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return DescribeListenerCertificates(ctx, descCertIn, nc, testAccountID) + return gateway_elbv2.DescribeListenerCertificates(ctx, descCertIn, nc, testAccountID) }, }, { name: "DescribeSSLPolicies", subject: "elbv2.DescribeSSLPolicies", input: sslIn, reply: sslOut, want: sslOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return DescribeSSLPolicies(ctx, sslIn, nc, testAccountID) + return gateway_elbv2.DescribeSSLPolicies(ctx, sslIn, nc, testAccountID) }, }, { name: "LBAgentHeartbeat", subject: "elbv2.LBAgentHeartbeat", input: heartbeatIn, reply: heartbeatOut, want: heartbeatOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return LBAgentHeartbeat(ctx, heartbeatIn, nc, testAccountID) + return gateway_elbv2.LBAgentHeartbeat(ctx, heartbeatIn, nc, testAccountID) }, }, { name: "GetLBConfig", subject: "elbv2.GetLBConfig", input: lbConfigIn, reply: lbConfigOut, want: lbConfigOut, call: func(ctx context.Context, nc *nats.Conn) (any, error) { - return GetLBConfig(ctx, lbConfigIn, nc, testAccountID) + return gateway_elbv2.GetLBConfig(ctx, lbConfigIn, nc, testAccountID) }, }, } @@ -452,136 +455,136 @@ func TestGatewayDispatch_ValidationGuards(t *testing.T) { call func() error }{ {"CreateRule/NilInput", awserrors.ErrorInvalidParameterValue, func() error { - _, err := CreateRule(context.Background(), nil, nil, testAccountID) + _, err := gateway_elbv2.CreateRule(context.Background(), nil, nil, testAccountID) return err }}, {"CreateRule/MissingListenerArn", awserrors.ErrorMissingParameter, func() error { - _, err := CreateRule(context.Background(), &elbv2.CreateRuleInput{}, nil, testAccountID) + _, err := gateway_elbv2.CreateRule(context.Background(), &elbv2.CreateRuleInput{}, nil, testAccountID) return err }}, {"CreateRule/MissingPriority", awserrors.ErrorMissingParameter, func() error { - _, err := CreateRule(context.Background(), &elbv2.CreateRuleInput{ - ListenerArn: aws.String(testListenerArn), + _, err := gateway_elbv2.CreateRule(context.Background(), &elbv2.CreateRuleInput{ + ListenerArn: aws.String(testLsnrArn), }, nil, testAccountID) return err }}, {"CreateRule/MissingConditions", awserrors.ErrorMissingParameter, func() error { - _, err := CreateRule(context.Background(), &elbv2.CreateRuleInput{ - ListenerArn: aws.String(testListenerArn), Priority: aws.Int64(1), + _, err := gateway_elbv2.CreateRule(context.Background(), &elbv2.CreateRuleInput{ + ListenerArn: aws.String(testLsnrArn), Priority: aws.Int64(1), }, nil, testAccountID) return err }}, {"CreateRule/MissingActions", awserrors.ErrorMissingParameter, func() error { - _, err := CreateRule(context.Background(), &elbv2.CreateRuleInput{ - ListenerArn: aws.String(testListenerArn), Priority: aws.Int64(1), + _, err := gateway_elbv2.CreateRule(context.Background(), &elbv2.CreateRuleInput{ + ListenerArn: aws.String(testLsnrArn), Priority: aws.Int64(1), Conditions: []*elbv2.RuleCondition{{Field: aws.String("path-pattern")}}, }, nil, testAccountID) return err }}, {"ModifyRule/NilInput", awserrors.ErrorInvalidParameterValue, func() error { - _, err := ModifyRule(context.Background(), nil, nil, testAccountID) + _, err := gateway_elbv2.ModifyRule(context.Background(), nil, nil, testAccountID) return err }}, {"ModifyRule/MissingRuleArn", awserrors.ErrorMissingParameter, func() error { - _, err := ModifyRule(context.Background(), &elbv2.ModifyRuleInput{}, nil, testAccountID) + _, err := gateway_elbv2.ModifyRule(context.Background(), &elbv2.ModifyRuleInput{}, nil, testAccountID) return err }}, // Either conditions or actions satisfies the guard; neither does not. {"ModifyRule/NoConditionsOrActions", awserrors.ErrorMissingParameter, func() error { - _, err := ModifyRule(context.Background(), &elbv2.ModifyRuleInput{ + _, err := gateway_elbv2.ModifyRule(context.Background(), &elbv2.ModifyRuleInput{ RuleArn: aws.String(testRuleArn), }, nil, testAccountID) return err }}, {"DeleteRule/NilInput", awserrors.ErrorInvalidParameterValue, func() error { - _, err := DeleteRule(context.Background(), nil, nil, testAccountID) + _, err := gateway_elbv2.DeleteRule(context.Background(), nil, nil, testAccountID) return err }}, {"DeleteRule/MissingRuleArn", awserrors.ErrorMissingParameter, func() error { - _, err := DeleteRule(context.Background(), &elbv2.DeleteRuleInput{}, nil, testAccountID) + _, err := gateway_elbv2.DeleteRule(context.Background(), &elbv2.DeleteRuleInput{}, nil, testAccountID) return err }}, {"DescribeRules/NilInput", awserrors.ErrorInvalidParameterValue, func() error { - _, err := DescribeRules(context.Background(), nil, nil, testAccountID) + _, err := gateway_elbv2.DescribeRules(context.Background(), nil, nil, testAccountID) return err }}, {"DescribeRules/NeitherListenerNorRules", awserrors.ErrorMissingParameter, func() error { - _, err := DescribeRules(context.Background(), &elbv2.DescribeRulesInput{}, nil, testAccountID) + _, err := gateway_elbv2.DescribeRules(context.Background(), &elbv2.DescribeRulesInput{}, nil, testAccountID) return err }}, {"SetRulePriorities/NilInput", awserrors.ErrorInvalidParameterValue, func() error { - _, err := SetRulePriorities(context.Background(), nil, nil, testAccountID) + _, err := gateway_elbv2.SetRulePriorities(context.Background(), nil, nil, testAccountID) return err }}, {"SetRulePriorities/MissingPriorities", awserrors.ErrorMissingParameter, func() error { - _, err := SetRulePriorities(context.Background(), &elbv2.SetRulePrioritiesInput{}, nil, testAccountID) + _, err := gateway_elbv2.SetRulePriorities(context.Background(), &elbv2.SetRulePrioritiesInput{}, nil, testAccountID) return err }}, {"AddTags/NilInput", awserrors.ErrorMissingParameter, func() error { - _, err := AddTags(context.Background(), nil, nil, testAccountID) + _, err := gateway_elbv2.AddTags(context.Background(), nil, nil, testAccountID) return err }}, {"AddTags/MissingTags", awserrors.ErrorMissingParameter, func() error { - _, err := AddTags(context.Background(), &elbv2.AddTagsInput{ + _, err := gateway_elbv2.AddTags(context.Background(), &elbv2.AddTagsInput{ ResourceArns: []*string{aws.String(testLBArn)}, }, nil, testAccountID) return err }}, {"RemoveTags/MissingTagKeys", awserrors.ErrorMissingParameter, func() error { - _, err := RemoveTags(context.Background(), &elbv2.RemoveTagsInput{ + _, err := gateway_elbv2.RemoveTags(context.Background(), &elbv2.RemoveTagsInput{ ResourceArns: []*string{aws.String(testLBArn)}, }, nil, testAccountID) return err }}, {"DescribeTags/MissingResourceArns", awserrors.ErrorMissingParameter, func() error { - _, err := DescribeTags(context.Background(), &elbv2.DescribeTagsInput{}, nil, testAccountID) + _, err := gateway_elbv2.DescribeTags(context.Background(), &elbv2.DescribeTagsInput{}, nil, testAccountID) return err }}, {"SetSecurityGroups/MissingLBArn", awserrors.ErrorMissingParameter, func() error { - _, err := SetSecurityGroups(context.Background(), &elbv2.SetSecurityGroupsInput{}, nil, testAccountID) + _, err := gateway_elbv2.SetSecurityGroups(context.Background(), &elbv2.SetSecurityGroupsInput{}, nil, testAccountID) return err }}, {"SetSecurityGroups/MissingGroups", awserrors.ErrorMissingParameter, func() error { - _, err := SetSecurityGroups(context.Background(), &elbv2.SetSecurityGroupsInput{ + _, err := gateway_elbv2.SetSecurityGroups(context.Background(), &elbv2.SetSecurityGroupsInput{ LoadBalancerArn: aws.String(testLBArn), }, nil, testAccountID) return err }}, {"SetIpAddressType/MissingLBArn", awserrors.ErrorMissingParameter, func() error { - _, err := SetIpAddressType(context.Background(), &elbv2.SetIpAddressTypeInput{}, nil, testAccountID) + _, err := gateway_elbv2.SetIpAddressType(context.Background(), &elbv2.SetIpAddressTypeInput{}, nil, testAccountID) return err }}, {"SetIpAddressType/MissingType", awserrors.ErrorMissingParameter, func() error { - _, err := SetIpAddressType(context.Background(), &elbv2.SetIpAddressTypeInput{ + _, err := gateway_elbv2.SetIpAddressType(context.Background(), &elbv2.SetIpAddressTypeInput{ LoadBalancerArn: aws.String(testLBArn), }, nil, testAccountID) return err }}, {"SetSubnets/MissingLBArn", awserrors.ErrorMissingParameter, func() error { - _, err := SetSubnets(context.Background(), &elbv2.SetSubnetsInput{}, nil, testAccountID) + _, err := gateway_elbv2.SetSubnets(context.Background(), &elbv2.SetSubnetsInput{}, nil, testAccountID) return err }}, // SubnetMappings is the accepted alternative to Subnets; neither fails. {"SetSubnets/NoSubnetsOrMappings", awserrors.ErrorMissingParameter, func() error { - _, err := SetSubnets(context.Background(), &elbv2.SetSubnetsInput{ + _, err := gateway_elbv2.SetSubnets(context.Background(), &elbv2.SetSubnetsInput{ LoadBalancerArn: aws.String(testLBArn), }, nil, testAccountID) return err }}, {"LBAgentHeartbeat/NilInput", awserrors.ErrorInvalidParameterValue, func() error { - _, err := LBAgentHeartbeat(context.Background(), nil, nil, testAccountID) + _, err := gateway_elbv2.LBAgentHeartbeat(context.Background(), nil, nil, testAccountID) return err }}, {"LBAgentHeartbeat/MissingLBID", awserrors.ErrorMissingParameter, func() error { - _, err := LBAgentHeartbeat(context.Background(), &handlers_elbv2.LBAgentHeartbeatInput{}, nil, testAccountID) + _, err := gateway_elbv2.LBAgentHeartbeat(context.Background(), &handlers_elbv2.LBAgentHeartbeatInput{}, nil, testAccountID) return err }}, {"GetLBConfig/NilInput", awserrors.ErrorInvalidParameterValue, func() error { - _, err := GetLBConfig(context.Background(), nil, nil, testAccountID) + _, err := gateway_elbv2.GetLBConfig(context.Background(), nil, nil, testAccountID) return err }}, {"GetLBConfig/MissingLBID", awserrors.ErrorMissingParameter, func() error { - _, err := GetLBConfig(context.Background(), &handlers_elbv2.GetLBConfigInput{}, nil, testAccountID) + _, err := gateway_elbv2.GetLBConfig(context.Background(), &handlers_elbv2.GetLBConfigInput{}, nil, testAccountID) return err }}, } @@ -600,7 +603,7 @@ func TestGatewayDispatch_ErrorEnvelopePropagates(t *testing.T) { serveOnce(t, nc, "elbv2.DeleteLoadBalancer", utils.GenerateErrorPayload(awserrors.ErrorELBv2LoadBalancerNotFound)) - _, err := DeleteLoadBalancer(context.Background(), + _, err := gateway_elbv2.DeleteLoadBalancer(context.Background(), &elbv2.DeleteLoadBalancerInput{LoadBalancerArn: aws.String(testLBArn)}, nc, testAccountID) require.EqualError(t, err, awserrors.ErrorELBv2LoadBalancerNotFound) } @@ -610,12 +613,12 @@ func TestGatewayDispatch_ErrorEnvelopePropagates(t *testing.T) { func TestGatewayDispatch_NoResponder(t *testing.T) { _, nc := testutil.StartTestNATS(t) - _, err := DescribeLoadBalancers(context.Background(), &elbv2.DescribeLoadBalancersInput{}, nc, testAccountID) + _, err := gateway_elbv2.DescribeLoadBalancers(context.Background(), &elbv2.DescribeLoadBalancersInput{}, nc, testAccountID) require.ErrorIs(t, err, nats.ErrNoResponders) } // A nil connection is refused before any request is attempted. func TestGatewayDispatch_NilConnection(t *testing.T) { - _, err := DescribeLoadBalancers(context.Background(), &elbv2.DescribeLoadBalancersInput{}, nil, testAccountID) + _, err := gateway_elbv2.DescribeLoadBalancers(context.Background(), &elbv2.DescribeLoadBalancersInput{}, nil, testAccountID) require.ErrorIs(t, err, utils.ErrClusterUnavailable) } diff --git a/spinifex/network/subscribers/topology_test.go b/spinifex/network/subscribers/topology_test.go new file mode 100644 index 000000000..77f834aa9 --- /dev/null +++ b/spinifex/network/subscribers/topology_test.go @@ -0,0 +1,403 @@ +//test:in-package — the handlers under test are unexported methods on +// Subscriber, and the tests reuse newTestSubscriber and the respondResponse +// envelope, both of which are package-internal. + +package subscribers + +import ( + "context" + "encoding/json" + "testing" + + "github.com/nats-io/nats.go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mulgadc/spinifex/spinifex/network/external" + "github.com/mulgadc/spinifex/spinifex/network/ovn/mock" + "github.com/mulgadc/spinifex/spinifex/network/ovn/nbdb" + "github.com/mulgadc/spinifex/spinifex/network/policy" + "github.com/mulgadc/spinifex/spinifex/network/topology" + "github.com/mulgadc/spinifex/spinifex/testutil" + "github.com/mulgadc/spinifex/spinifex/types" +) + +// runningSubscriber wires a subscriber to a live NATS server with every topic +// registered, so tests drive handlers the way vpcd does. +func runningSubscriber(t *testing.T) (*nats.Conn, *Subscriber, *mock.Client) { + t.Helper() + _, nc := testutil.StartTestNATS(t) + sub, m := newTestSubscriber(t) + subs, err := sub.Subscribe(nc) + require.NoError(t, err) + t.Cleanup(func() { + for _, s := range subs { + _ = s.Unsubscribe() + } + }) + return nc, sub, m +} + +// requestOK publishes evt and requires the handler's reply envelope to report +// success, so a handler that errors fails here rather than in a state assertion. +func requestOK(t *testing.T, nc *nats.Conn, topic string, evt any) { + t.Helper() + data, err := json.Marshal(evt) + require.NoError(t, err) + resp, err := nc.Request(topic, data, requestTimeout) + require.NoError(t, err) + var env respondResponse + require.NoError(t, json.Unmarshal(resp.Data, &env)) + require.True(t, env.Success, "%s: %s", topic, env.Error) +} + +// requestFails is requestOK's negative: the handler must reject the event and +// say so in the envelope. +func requestFails(t *testing.T, nc *nats.Conn, topic string, evt any) string { + t.Helper() + data, err := json.Marshal(evt) + require.NoError(t, err) + resp, err := nc.Request(topic, data, requestTimeout) + require.NoError(t, err) + var env respondResponse + require.NoError(t, json.Unmarshal(resp.Data, &env)) + require.False(t, env.Success, "%s must reject the event", topic) + return env.Error +} + +// The mock reports a missing row as an error rather than a nil result, so +// absence is asserted against the list APIs. + +func routerExists(t *testing.T, m *mock.Client, name string) bool { + t.Helper() + rows, err := m.ListLogicalRouters(context.Background()) + require.NoError(t, err) + for _, r := range rows { + if r.Name == name { + return true + } + } + return false +} + +func switchExists(t *testing.T, m *mock.Client, name string) bool { + t.Helper() + rows, err := m.ListLogicalSwitches(context.Background()) + require.NoError(t, err) + for _, s := range rows { + if s.Name == name { + return true + } + } + return false +} + +func switchPortExists(t *testing.T, m *mock.Client, name string) bool { + t.Helper() + rows, err := m.ListLogicalSwitchPorts(context.Background()) + require.NoError(t, err) + for _, p := range rows { + if p.Name == name { + return true + } + } + return false +} + +func routerPortExists(t *testing.T, m *mock.Client, name string) bool { + t.Helper() + rows, err := m.ListLogicalRouterPorts(context.Background()) + require.NoError(t, err) + for _, p := range rows { + if p.Name == name { + return true + } + } + return false +} + +// seedVPCWithIGW creates the router and attaches an IGW, the precondition for +// every egress-policy handler. +func seedVPCWithIGW(t *testing.T, sub *Subscriber, m *mock.Client, vpcID string) { + t.Helper() + ctx := context.Background() + require.NoError(t, m.CreateLogicalRouter(ctx, &nbdb.LogicalRouter{ + Name: topology.VPCRouter(vpcID), + ExternalIDs: map[string]string{"spinifex:vpc_id": vpcID}, + })) + require.NoError(t, sub.igw.AttachIGW(ctx, external.IGWSpec{VPCID: vpcID, InternetGatewayID: "igw-1"})) +} + +func TestHandleVPC_CreateAndDelete(t *testing.T) { + ctx := context.Background() + nc, _, m := runningSubscriber(t) + + requestOK(t, nc, TopicVPCCreate, VPCEvent{VpcId: "vpc-1", CidrBlock: "10.0.0.0/16", VNI: 7}) + + lr, err := m.GetLogicalRouter(ctx, topology.VPCRouter("vpc-1")) + require.NoError(t, err) + require.NotNil(t, lr, "vpc.create must create the VPC logical router") + assert.Equal(t, "vpc-1", lr.ExternalIDs["spinifex:vpc_id"]) + + requestOK(t, nc, TopicVPCDelete, VPCEvent{VpcId: "vpc-1"}) + + assert.False(t, routerExists(t, m, topology.VPCRouter("vpc-1")), + "vpc.delete must remove the VPC logical router") +} + +// A CIDR that will not parse must be rejected before any OVN write. +func TestHandleVPCCreate_InvalidCIDR(t *testing.T) { + nc, _, m := runningSubscriber(t) + + requestFails(t, nc, TopicVPCCreate, VPCEvent{VpcId: "vpc-1", CidrBlock: "not-a-cidr"}) + + assert.False(t, routerExists(t, m, topology.VPCRouter("vpc-1")), + "invalid CIDR must not create a router") +} + +// vpc.create and vpc.create-subnet have no ordering guarantee, so the subnet +// handler pre-ensures the router itself. +func TestHandleSubnetCreate_PreEnsuresVPCRouter(t *testing.T) { + ctx := context.Background() + nc, _, m := runningSubscriber(t) + + requestOK(t, nc, TopicSubnetCreate, SubnetEvent{ + SubnetId: "subnet-1", VpcId: "vpc-1", CidrBlock: "10.0.1.0/24", + }) + + lr, err := m.GetLogicalRouter(ctx, topology.VPCRouter("vpc-1")) + require.NoError(t, err) + require.NotNil(t, lr, "subnet create must pre-ensure the VPC router") + + require.True(t, switchExists(t, m, topology.SubnetSwitch("subnet-1")), + "subnet create must create the subnet switch") + + lrp, err := m.GetLogicalRouterPort(ctx, topology.SubnetRouterPort("subnet-1")) + require.NoError(t, err) + require.NotNil(t, lrp, "subnet create must attach the subnet to its router") + assert.Contains(t, lrp.Networks, "10.0.1.1/24", "router port takes the subnet's first usable address") + + requestOK(t, nc, TopicSubnetDelete, SubnetEvent{ + SubnetId: "subnet-1", VpcId: "vpc-1", CidrBlock: "10.0.1.0/24", + }) + + assert.False(t, switchExists(t, m, topology.SubnetSwitch("subnet-1")), + "subnet delete must remove the subnet switch") +} + +func TestHandleSubnetCreate_InvalidCIDR(t *testing.T) { + nc, _, m := runningSubscriber(t) + + requestFails(t, nc, TopicSubnetCreate, SubnetEvent{ + SubnetId: "subnet-1", VpcId: "vpc-1", CidrBlock: "10.0.1.0/33", + }) + + assert.False(t, switchExists(t, m, topology.SubnetSwitch("subnet-1"))) +} + +// delete-subnet tolerates a missing CIDR: the spec is keyed by ID, and the +// CIDR is only used to release the DHCP options row. +func TestHandleSubnetDelete_WithoutCIDR(t *testing.T) { + nc, _, m := runningSubscriber(t) + + requestOK(t, nc, TopicSubnetCreate, SubnetEvent{ + SubnetId: "subnet-1", VpcId: "vpc-1", CidrBlock: "10.0.1.0/24", + }) + requestOK(t, nc, TopicSubnetDelete, SubnetEvent{SubnetId: "subnet-1", VpcId: "vpc-1"}) + + assert.False(t, switchExists(t, m, topology.SubnetSwitch("subnet-1"))) +} + +func TestHandlePort_CreateAndDelete(t *testing.T) { + ctx := context.Background() + nc, sub, m := runningSubscriber(t) + + requestOK(t, nc, TopicSubnetCreate, SubnetEvent{ + SubnetId: "subnet-1", VpcId: "vpc-1", CidrBlock: "10.0.1.0/24", + }) + require.NoError(t, sub.topology.EnsureSGPortGroup(ctx, "sg-1")) + + requestOK(t, nc, TopicCreatePort, PortEvent{ + NetworkInterfaceId: "eni-1", SubnetId: "subnet-1", VpcId: "vpc-1", + PrivateIpAddress: "10.0.1.10", MacAddress: "02:00:00:00:00:01", + SecurityGroupIds: []string{"sg-1"}, + }) + + lsp, err := m.GetLogicalSwitchPort(ctx, topology.Port("eni-1")) + require.NoError(t, err) + require.NotNil(t, lsp, "create-port must create the logical switch port") + assert.Contains(t, lsp.Addresses, "02:00:00:00:00:01 10.0.1.10") + + pgs, err := m.ListPortGroupsForPort(ctx, topology.Port("eni-1")) + require.NoError(t, err) + assert.Contains(t, pgs, topology.SecurityGroupPortGroup("sg-1"), + "create-port must join the port to its security group") + + requestOK(t, nc, TopicDeletePort, PortEvent{ + NetworkInterfaceId: "eni-1", SubnetId: "subnet-1", VpcId: "vpc-1", + }) + + assert.False(t, switchPortExists(t, m, topology.Port("eni-1")), + "delete-port must remove the logical switch port") +} + +// Address fields are parsed before the OVN write, so a malformed IP or MAC is +// reported rather than half-applied. +func TestHandleCreatePort_MalformedAddresses(t *testing.T) { + nc, _, m := runningSubscriber(t) + + requestOK(t, nc, TopicSubnetCreate, SubnetEvent{ + SubnetId: "subnet-1", VpcId: "vpc-1", CidrBlock: "10.0.1.0/24", + }) + + requestFails(t, nc, TopicCreatePort, PortEvent{ + NetworkInterfaceId: "eni-bad-ip", SubnetId: "subnet-1", VpcId: "vpc-1", + PrivateIpAddress: "999.0.0.1", MacAddress: "02:00:00:00:00:01", + }) + requestFails(t, nc, TopicCreatePort, PortEvent{ + NetworkInterfaceId: "eni-bad-mac", SubnetId: "subnet-1", VpcId: "vpc-1", + PrivateIpAddress: "10.0.1.10", MacAddress: "not-a-mac", + }) + + for _, name := range []string{"eni-bad-ip", "eni-bad-mac"} { + assert.False(t, switchPortExists(t, m, topology.Port(name)), "%s must not be created", name) + } +} + +// update-port-sgs is declarative: the handler passes the full desired list and +// the manager diffs it, so a shrinking list drops memberships. +func TestHandleUpdatePortSGs_ReplacesMemberships(t *testing.T) { + ctx := context.Background() + nc, sub, m := runningSubscriber(t) + + requestOK(t, nc, TopicSubnetCreate, SubnetEvent{ + SubnetId: "subnet-1", VpcId: "vpc-1", CidrBlock: "10.0.1.0/24", + }) + for _, sg := range []string{"sg-1", "sg-2"} { + require.NoError(t, sub.topology.EnsureSGPortGroup(ctx, sg)) + } + requestOK(t, nc, TopicCreatePort, PortEvent{ + NetworkInterfaceId: "eni-1", SubnetId: "subnet-1", VpcId: "vpc-1", + PrivateIpAddress: "10.0.1.10", MacAddress: "02:00:00:00:00:01", + SecurityGroupIds: []string{"sg-1"}, + }) + + requestOK(t, nc, TopicUpdatePortSGs, UpdatePortSGsEvent{ + NetworkInterfaceId: "eni-1", PrivateIpAddress: "10.0.1.10", + SecurityGroupIds: []string{"sg-2"}, + }) + + pgs, err := m.ListPortGroupsForPort(ctx, topology.Port("eni-1")) + require.NoError(t, err) + assert.Contains(t, pgs, topology.SecurityGroupPortGroup("sg-2")) + assert.NotContains(t, pgs, topology.SecurityGroupPortGroup("sg-1"), + "a group absent from the event must be removed") +} + +func TestHandleIGW_AttachAndDetach(t *testing.T) { + nc, _, m := runningSubscriber(t) + + requestOK(t, nc, TopicVPCCreate, VPCEvent{VpcId: "vpc-1", CidrBlock: "10.0.0.0/16"}) + requestOK(t, nc, TopicIGWAttach, types.IGWEvent{VpcId: "vpc-1", InternetGatewayId: "igw-1"}) + + require.True(t, routerPortExists(t, m, topology.GatewayRouterPort("vpc-1")), + "igw-attach must create the gateway router port") + + requestOK(t, nc, TopicIGWDetach, types.IGWEvent{VpcId: "vpc-1", InternetGatewayId: "igw-1"}) + + assert.False(t, routerPortExists(t, m, topology.GatewayRouterPort("vpc-1")), + "igw-detach must remove the gateway router port") +} + +func TestHandleNAT_AddAndDelete(t *testing.T) { + ctx := context.Background() + nc, sub, m := runningSubscriber(t) + seedVPCWithIGW(t, sub, m, "vpc-1") + + evt := NATEvent{ + VpcId: "vpc-1", ExternalIP: "192.168.1.70", LogicalIP: "10.0.1.10", + PortName: topology.Port("eni-1"), MAC: "02:00:00:00:00:01", + } + requestOK(t, nc, TopicAddNAT, evt) + + nat, err := m.FindNATByExternalIP(ctx, "dnat_and_snat", "192.168.1.70") + require.NoError(t, err) + require.NotNil(t, nat, "add-nat must install a dnat_and_snat rule") + assert.Equal(t, "10.0.1.10", nat.LogicalIP) + + requestOK(t, nc, TopicDeleteNAT, evt) + + nat, err = m.FindNATByExternalIP(ctx, "dnat_and_snat", "192.168.1.70") + require.NoError(t, err) + assert.Nil(t, nat, "delete-nat must remove the rule") +} + +// delete-igw-route must remove exactly the policy add-igw-route installed. +func TestHandleIGWRoute_DeleteRemovesPolicy(t *testing.T) { + ctx := context.Background() + nc, sub, m := runningSubscriber(t) + seedVPCWithIGW(t, sub, m, "vpc-1") + + evt := IGWRouteEvent{ + VpcId: "vpc-1", SubnetId: "subnet-pub", + DestinationCidr: "0.0.0.0/0", InternetGatewayId: "igw-1", + } + requestOK(t, nc, TopicAddIGWRoute, evt) + + policies, err := m.ListLogicalRouterPolicies(ctx, topology.VPCRouter("vpc-1")) + require.NoError(t, err) + require.Len(t, policies, 1) + + requestOK(t, nc, TopicDeleteIGWRoute, evt) + + policies, err = m.ListLogicalRouterPolicies(ctx, topology.VPCRouter("vpc-1")) + require.NoError(t, err) + assert.Empty(t, policies, "delete-igw-route must remove the egress policy") +} + +func TestHandleDeleteIGWRoute_InvalidCIDR(t *testing.T) { + nc, sub, m := runningSubscriber(t) + seedVPCWithIGW(t, sub, m, "vpc-1") + + requestFails(t, nc, TopicDeleteIGWRoute, IGWRouteEvent{ + VpcId: "vpc-1", SubnetId: "subnet-pub", DestinationCidr: "not-a-cidr", + }) +} + +// Gating installs a DROP above both reroute priorities; ungating removes it. +func TestHandleSubnetEgress_GateAndUngate(t *testing.T) { + ctx := context.Background() + nc, sub, m := runningSubscriber(t) + seedVPCWithIGW(t, sub, m, "vpc-1") + + gate := SubnetEgressGateEvent{VpcId: "vpc-1", SubnetId: "subnet-1", DestinationCidr: "0.0.0.0/0"} + requestOK(t, nc, TopicGateSubnetEgress, gate) + + policies, err := m.ListLogicalRouterPolicies(ctx, topology.VPCRouter("vpc-1")) + require.NoError(t, err) + require.Len(t, policies, 1) + assert.Equal(t, policy.SubnetEgressPriorityDrop, policies[0].Priority) + assert.Contains(t, policies[0].Match, topology.SubnetRouterPort("subnet-1")) + + requestOK(t, nc, TopicUngateSubnetEgress, + SubnetEgressUngateEvent{VpcId: "vpc-1", SubnetId: "subnet-1", DestinationCidr: "0.0.0.0/0"}) + + policies, err = m.ListLogicalRouterPolicies(ctx, topology.VPCRouter("vpc-1")) + require.NoError(t, err) + assert.Empty(t, policies, "ungate must remove the DROP policy") +} + +func TestHandleSubnetEgress_InvalidCIDR(t *testing.T) { + ctx := context.Background() + nc, sub, m := runningSubscriber(t) + seedVPCWithIGW(t, sub, m, "vpc-1") + + requestFails(t, nc, TopicGateSubnetEgress, + SubnetEgressGateEvent{VpcId: "vpc-1", SubnetId: "subnet-1", DestinationCidr: "not-a-cidr"}) + requestFails(t, nc, TopicUngateSubnetEgress, + SubnetEgressUngateEvent{VpcId: "vpc-1", SubnetId: "subnet-1", DestinationCidr: "not-a-cidr"}) + + policies, err := m.ListLogicalRouterPolicies(ctx, topology.VPCRouter("vpc-1")) + require.NoError(t, err) + assert.Empty(t, policies) +}