diff --git a/pkg/controllers/subnet/subnet_controller_test.go b/pkg/controllers/subnet/subnet_controller_test.go index 632b6d8f4..21b5645ad 100644 --- a/pkg/controllers/subnet/subnet_controller_test.go +++ b/pkg/controllers/subnet/subnet_controller_test.go @@ -10,6 +10,7 @@ import ( "github.com/agiledragon/gomonkey/v2" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/vmware/vsphere-automation-sdk-go/services/nsxt/model" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -803,3 +804,193 @@ func TestStartSubnetController(t *testing.T) { }) } } + +func TestReconcileWithSubnetConnectionBindingMaps(t *testing.T) { + subnetName := "subnet1" + ns := "ns1" + testSubnet1 := &v1alpha1.Subnet{ + ObjectMeta: metav1.ObjectMeta{Name: subnetName, Namespace: ns}, + Spec: v1alpha1.SubnetSpec{ + AccessMode: v1alpha1.AccessMode(v1alpha1.AccessModePrivate), + IPv4SubnetSize: 16, + }, + } + testSubnet2 := &v1alpha1.Subnet{ + ObjectMeta: metav1.ObjectMeta{Name: subnetName, Namespace: ns, Finalizers: []string{common.SubnetFinalizerName}}, + Spec: v1alpha1.SubnetSpec{ + AccessMode: v1alpha1.AccessMode(v1alpha1.AccessModePrivate), + IPv4SubnetSize: 16, + }, + } + deletionTime := metav1.Now() + testSubnet3 := &v1alpha1.Subnet{ + ObjectMeta: metav1.ObjectMeta{ + Name: subnetName, + Namespace: ns, + Finalizers: []string{common.SubnetFinalizerName}, + DeletionTimestamp: &deletionTime, + }, + Spec: v1alpha1.SubnetSpec{ + AccessMode: v1alpha1.AccessMode(v1alpha1.AccessModePrivate), + IPv4SubnetSize: 16, + }, + } + for _, tc := range []struct { + name string + existingSubnet *v1alpha1.Subnet + patches func(t *testing.T, r *SubnetReconciler) *gomonkey.Patches + expectErrStr string + expectRes ctrl.Result + }{ + { + name: "Successfully add finalizer after a Subnet is used by SubnetConnectionBindingMap", + existingSubnet: testSubnet1, + patches: func(t *testing.T, r *SubnetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyPrivateMethod(reflect.TypeOf(r), "getSubnetBindingCRsBySubnet", func(_ *SubnetReconciler, _ context.Context, _ *v1alpha1.Subnet) []v1alpha1.SubnetConnectionBindingMap { + return []v1alpha1.SubnetConnectionBindingMap{{ObjectMeta: metav1.ObjectMeta{Name: "binding1", Namespace: ns}}} + }) + patches.ApplyMethod(reflect.TypeOf(r.Client), "Update", func(_ client.Client, _ context.Context, obj client.Object, opts ...client.UpdateOption) error { + return nil + }) + return patchSuccessfulReconcileSubnetWorkflow(r, patches) + }, + expectRes: ctrl.Result{}, + }, { + name: "Failed to add finalizer after a Subnet is used by SubnetConnectionBindingMap", + existingSubnet: testSubnet1, + patches: func(t *testing.T, r *SubnetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyPrivateMethod(reflect.TypeOf(r), "getSubnetBindingCRsBySubnet", func(_ *SubnetReconciler, _ context.Context, _ *v1alpha1.Subnet) []v1alpha1.SubnetConnectionBindingMap { + return []v1alpha1.SubnetConnectionBindingMap{{ObjectMeta: metav1.ObjectMeta{Name: "binding1", Namespace: ns}}} + }) + patches.ApplyMethod(reflect.TypeOf(r.Client), "Update", func(_ client.Client, _ context.Context, obj client.Object, opts ...client.UpdateOption) error { + return fmt.Errorf("failed to update CR") + }) + patches.ApplyFunc(updateSubnetStatusConditions, func(_ client.Client, _ context.Context, _ *v1alpha1.Subnet, newConditions []v1alpha1.Condition) { + require.Equal(t, 1, len(newConditions)) + cond := newConditions[0] + assert.Equal(t, "Failed to add the finalizer on a Subnet for the reference by SubnetConnectionBindingMap binding1", cond.Message) + }) + return patches + }, + expectErrStr: "failed to update CR", + expectRes: common2.ResultRequeue, + }, { + name: "Not add duplicated finalizer after a Subnet is used by SubnetConnectionBindingMap", + existingSubnet: testSubnet2, + patches: func(t *testing.T, r *SubnetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyPrivateMethod(reflect.TypeOf(r), "getSubnetBindingCRsBySubnet", func(_ *SubnetReconciler, _ context.Context, _ *v1alpha1.Subnet) []v1alpha1.SubnetConnectionBindingMap { + return []v1alpha1.SubnetConnectionBindingMap{{ObjectMeta: metav1.ObjectMeta{Name: "binding1", Namespace: ns}}} + }) + patches.ApplyMethod(reflect.TypeOf(r.Client), "Update", func(_ client.Client, _ context.Context, obj client.Object, opts ...client.UpdateOption) error { + assert.FailNow(t, "Should not update Subnet CR finalizer") + return nil + }) + return patchSuccessfulReconcileSubnetWorkflow(r, patches) + }, + expectRes: ctrl.Result{}, + }, { + name: "Successfully remove finalizer after a Subnet is not used by any SubnetConnectionBindingMaps", + existingSubnet: testSubnet2, + patches: func(t *testing.T, r *SubnetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyPrivateMethod(reflect.TypeOf(r), "getSubnetBindingCRsBySubnet", func(_ *SubnetReconciler, _ context.Context, _ *v1alpha1.Subnet) []v1alpha1.SubnetConnectionBindingMap { + return []v1alpha1.SubnetConnectionBindingMap{} + }) + patches.ApplyMethod(reflect.TypeOf(r.Client), "Update", func(_ client.Client, _ context.Context, obj client.Object, opts ...client.UpdateOption) error { + return nil + }) + return patchSuccessfulReconcileSubnetWorkflow(r, patches) + }, + expectRes: ctrl.Result{}, + }, { + name: "Failed to remove finalizer after a Subnet is not used by any SubnetConnectionBindingMaps", + existingSubnet: testSubnet2, + patches: func(t *testing.T, r *SubnetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyPrivateMethod(reflect.TypeOf(r), "getSubnetBindingCRsBySubnet", func(_ *SubnetReconciler, _ context.Context, _ *v1alpha1.Subnet) []v1alpha1.SubnetConnectionBindingMap { + return []v1alpha1.SubnetConnectionBindingMap{} + }) + patches.ApplyMethod(reflect.TypeOf(r.Client), "Update", func(_ client.Client, _ context.Context, obj client.Object, opts ...client.UpdateOption) error { + return fmt.Errorf("failed to update CR") + }) + patches.ApplyFunc(updateSubnetStatusConditions, func(_ client.Client, _ context.Context, _ *v1alpha1.Subnet, newConditions []v1alpha1.Condition) { + require.Equal(t, 1, len(newConditions)) + cond := newConditions[0] + assert.Equal(t, "Failed to remove the finalizer on a Subnet when there is no reference by SubnetConnectionBindingMaps", cond.Message) + }) + return patches + }, + expectErrStr: "failed to update CR", + expectRes: common2.ResultRequeue, + }, { + name: "Not update finalizers if a Subnet is not used by any SubnetConnectionBindingMaps", + existingSubnet: testSubnet1, + patches: func(t *testing.T, r *SubnetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyPrivateMethod(reflect.TypeOf(r), "getSubnetBindingCRsBySubnet", func(_ *SubnetReconciler, _ context.Context, _ *v1alpha1.Subnet) []v1alpha1.SubnetConnectionBindingMap { + return []v1alpha1.SubnetConnectionBindingMap{} + }) + patches.ApplyMethod(reflect.TypeOf(r.Client), "Update", func(_ client.Client, _ context.Context, obj client.Object, opts ...client.UpdateOption) error { + assert.FailNow(t, "Should not update Subnet CR finalizer") + return nil + }) + return patchSuccessfulReconcileSubnetWorkflow(r, patches) + }, + expectRes: ctrl.Result{}, + }, { + name: "Delete a Subnet is not allowed if it is used by SubnetConnectionBindingMap", + existingSubnet: testSubnet3, + patches: func(t *testing.T, r *SubnetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyPrivateMethod(reflect.TypeOf(r), "getSubnetBindingCRsBySubnet", func(_ *SubnetReconciler, _ context.Context, _ *v1alpha1.Subnet) []v1alpha1.SubnetConnectionBindingMap { + return []v1alpha1.SubnetConnectionBindingMap{} + }) + patches.ApplyPrivateMethod(reflect.TypeOf(r), "getNSXSubnetBindingsBySubnet", func(_ *SubnetReconciler, _ string) []*v1alpha1.SubnetConnectionBindingMap { + return []*v1alpha1.SubnetConnectionBindingMap{{ObjectMeta: metav1.ObjectMeta{Name: "binding1", Namespace: ns}}} + }) + patches.ApplyPrivateMethod(reflect.TypeOf(r), "setSubnetDeletionFailedStatus", func(_ *SubnetReconciler, _ context.Context, _ *v1alpha1.Subnet, _ metav1.Time, msg string, reason string) { + assert.Equal(t, "Subnet is used by SubnetConnectionBindingMap binding1 and not able to delete", msg) + assert.Equal(t, "SubnetInUse", reason) + }) + return patches + }, + expectErrStr: "failed to delete Subnet CR ns1/subnet1", + expectRes: ResultRequeue, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := context.TODO() + req := ctrl.Request{NamespacedName: types.NamespacedName{Name: subnetName, Namespace: ns}} + r := createFakeSubnetReconciler([]client.Object{tc.existingSubnet}) + if tc.patches != nil { + patches := tc.patches(t, r) + defer patches.Reset() + } + + res, err := r.Reconcile(ctx, req) + + if tc.expectErrStr != "" { + assert.EqualError(t, err, tc.expectErrStr) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tc.expectRes, res) + }) + } +} + +func patchSuccessfulReconcileSubnetWorkflow(r *SubnetReconciler, patches *gomonkey.Patches) *gomonkey.Patches { + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "GenerateSubnetNSTags", func(_ *subnet.SubnetService, _ client.Object) []model.Tag { + return []model.Tag{{Scope: common.String("test"), Tag: common.String("subnet")}} + }) + patches.ApplyMethod(reflect.TypeOf(r.VPCService), "ListVPCInfo", func(_ *vpc.VPCService, _ string) []common.VPCResourceInfo { + return []common.VPCResourceInfo{{ID: "vpc1"}} + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "CreateOrUpdateSubnet", func(_ *subnet.SubnetService, _ client.Object, _ common.VPCResourceInfo, _ []model.Tag) (subnet *model.VpcSubnet, err error) { + return &model.VpcSubnet{ + Path: common.String("subnet-path"), + }, nil + }) + patches.ApplyPrivateMethod(reflect.TypeOf(r), "updateSubnetStatus", func(_ *SubnetReconciler, _ *v1alpha1.Subnet) error { + return nil + }) + patches.ApplyFunc(setSubnetReadyStatusTrue, func(_ client.Client, _ context.Context, _ client.Object, _ metav1.Time, _ ...interface{}) { + }) + return patches +} diff --git a/pkg/controllers/subnet/subnetbinding_handler_test.go b/pkg/controllers/subnet/subnetbinding_handler_test.go index d11aaa543..deec1d674 100644 --- a/pkg/controllers/subnet/subnetbinding_handler_test.go +++ b/pkg/controllers/subnet/subnetbinding_handler_test.go @@ -2,10 +2,13 @@ package subnet import ( "context" + "reflect" "testing" + "github.com/agiledragon/gomonkey/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vmware/vsphere-automation-sdk-go/services/nsxt/model" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -16,6 +19,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/vmware-tanzu/nsx-operator/pkg/apis/vpc/v1alpha1" + "github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/common" + "github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/subnet" + "github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/subnetbinding" ) var ( @@ -156,3 +162,134 @@ func queueItemEquals(t *testing.T, myQueue workqueue.TypedRateLimitingInterface[ assert.Equal(t, req, item) myQueue.Done(item) } + +func TestGetSubnetBindingCRsBySubnet(t *testing.T) { + binding1 := &v1alpha1.SubnetConnectionBindingMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding1", + Namespace: "default", + }, + Spec: v1alpha1.SubnetConnectionBindingMapSpec{ + SubnetName: "subnet1", + TargetSubnetName: "subnet2", + VLANTrafficTag: 201, + }, + } + binding2 := &v1alpha1.SubnetConnectionBindingMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding2", + Namespace: "default", + }, + Spec: v1alpha1.SubnetConnectionBindingMapSpec{ + SubnetName: "subnet2", + TargetSubnetName: "subnet3", + VLANTrafficTag: 201, + }, + } + newScheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(newScheme)) + utilruntime.Must(v1alpha1.AddToScheme(newScheme)) + fakeClient := fake.NewClientBuilder().WithScheme(newScheme).WithObjects(binding1, binding2).Build() + r := &SubnetReconciler{ + Client: fakeClient, + } + for _, tc := range []struct { + name string + subnetCR *v1alpha1.Subnet + expSubnetConnectionBindingMaps []v1alpha1.SubnetConnectionBindingMap + }{ + { + name: "Success case to list all SubnetConnectionBindingMaps using the Subnet as either CHILD or PARENT", + subnetCR: &v1alpha1.Subnet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "subnet2", + Namespace: "default", + }, + }, + expSubnetConnectionBindingMaps: []v1alpha1.SubnetConnectionBindingMap{*binding1, *binding2}, + }, + { + name: "No SubnetConnectionBindingMap found", + subnetCR: &v1alpha1.Subnet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "subnet4", + Namespace: "default", + }, + }, + expSubnetConnectionBindingMaps: []v1alpha1.SubnetConnectionBindingMap{}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + actBindings := r.getSubnetBindingCRsBySubnet(ctx, tc.subnetCR) + assert.ElementsMatch(t, tc.expSubnetConnectionBindingMaps, actBindings) + }) + } +} + +func TestGetNSXSubnetBindingsBySubnet(t *testing.T) { + for _, tc := range []struct { + name string + subnetCRUID string + patches func(r *SubnetReconciler) *gomonkey.Patches + expSubnetConnectionBindingMaps []*v1alpha1.SubnetConnectionBindingMap + }{ + { + name: "No NSX VpcSubnet exists for the Subnet CR", + subnetCRUID: "uuid1", + patches: func(r *SubnetReconciler) *gomonkey.Patches { + patch := gomonkey.ApplyMethod(reflect.TypeOf(r.SubnetService), "ListSubnetCreatedBySubnet", func(_ *subnet.SubnetService, _ string) []*model.VpcSubnet { + return []*model.VpcSubnet{} + }) + return patch + }, + expSubnetConnectionBindingMaps: nil, + }, { + name: "No NSX SubnetConnectionBindingMap created for VpcSubnet", + subnetCRUID: "uuid1", + patches: func(r *SubnetReconciler) *gomonkey.Patches { + patch := gomonkey.ApplyMethod(reflect.TypeOf(r.SubnetService), "ListSubnetCreatedBySubnet", func(_ *subnet.SubnetService, _ string) []*model.VpcSubnet { + return []*model.VpcSubnet{ + {Id: common.String("id1")}, + } + }) + patch.ApplyMethod(reflect.TypeOf(r.BindingService), "GetSubnetConnectionBindingMapCRsBySubnet", func(_ *subnetbinding.BindingService, _ *model.VpcSubnet) []*v1alpha1.SubnetConnectionBindingMap { + return []*v1alpha1.SubnetConnectionBindingMap{} + }) + return patch + }, + expSubnetConnectionBindingMaps: nil, + }, { + name: "Partial of VpcSubnets are associated with the NSX SubnetConnectionBindingMap", + subnetCRUID: "uuid1", + patches: func(r *SubnetReconciler) *gomonkey.Patches { + patch := gomonkey.ApplyMethod(reflect.TypeOf(r.SubnetService), "ListSubnetCreatedBySubnet", func(_ *subnet.SubnetService, _ string) []*model.VpcSubnet { + return []*model.VpcSubnet{ + {Id: common.String("id1")}, + {Id: common.String("id2")}, + } + }) + patch.ApplyMethod(reflect.TypeOf(r.BindingService), "GetSubnetConnectionBindingMapCRsBySubnet", func(_ *subnetbinding.BindingService, subnet *model.VpcSubnet) []*v1alpha1.SubnetConnectionBindingMap { + if *subnet.Id == "id1" { + return []*v1alpha1.SubnetConnectionBindingMap{} + } + return []*v1alpha1.SubnetConnectionBindingMap{bm1} + }) + return patch + }, + expSubnetConnectionBindingMaps: []*v1alpha1.SubnetConnectionBindingMap{bm1}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + r := &SubnetReconciler{ + SubnetService: &subnet.SubnetService{}, + BindingService: &subnetbinding.BindingService{}, + } + patches := tc.patches(r) + defer patches.Reset() + + actBindings := r.getNSXSubnetBindingsBySubnet(tc.subnetCRUID) + assert.ElementsMatch(t, tc.expSubnetConnectionBindingMaps, actBindings) + }) + } +} diff --git a/pkg/controllers/subnetset/subnetbinding_handler_test.go b/pkg/controllers/subnetset/subnetbinding_handler_test.go index 8ace1d949..d5dbaa302 100644 --- a/pkg/controllers/subnetset/subnetbinding_handler_test.go +++ b/pkg/controllers/subnetset/subnetbinding_handler_test.go @@ -2,10 +2,13 @@ package subnetset import ( "context" + "reflect" "testing" + "github.com/agiledragon/gomonkey/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vmware/vsphere-automation-sdk-go/services/nsxt/model" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -16,6 +19,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/vmware-tanzu/nsx-operator/pkg/apis/vpc/v1alpha1" + "github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/common" + "github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/subnet" + "github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/subnetbinding" ) var ( @@ -132,3 +138,134 @@ func queueItemEquals(t *testing.T, myQueue workqueue.TypedRateLimitingInterface[ assert.Equal(t, req, item) myQueue.Done(item) } + +func TestGetSubnetBindingCRsBySubnet(t *testing.T) { + binding1 := &v1alpha1.SubnetConnectionBindingMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding1", + Namespace: "default", + }, + Spec: v1alpha1.SubnetConnectionBindingMapSpec{ + SubnetName: "subnet1", + TargetSubnetSetName: "subnetset1", + VLANTrafficTag: 201, + }, + } + binding2 := &v1alpha1.SubnetConnectionBindingMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding2", + Namespace: "default", + }, + Spec: v1alpha1.SubnetConnectionBindingMapSpec{ + SubnetName: "subnet2", + TargetSubnetName: "subnet1", + VLANTrafficTag: 201, + }, + } + newScheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(newScheme)) + utilruntime.Must(v1alpha1.AddToScheme(newScheme)) + fakeClient := fake.NewClientBuilder().WithScheme(newScheme).WithObjects(binding1, binding2).Build() + r := &SubnetSetReconciler{ + Client: fakeClient, + } + for _, tc := range []struct { + name string + subnetSetCR *v1alpha1.SubnetSet + expSubnetConnectionBindingMaps []v1alpha1.SubnetConnectionBindingMap + }{ + { + name: "Success case to list SubnetConnectionBindingMaps using the target SubnetSet", + subnetSetCR: &v1alpha1.SubnetSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "subnetset1", + Namespace: "default", + }, + }, + expSubnetConnectionBindingMaps: []v1alpha1.SubnetConnectionBindingMap{*binding1}, + }, + { + name: "No SubnetConnectionBindingMap found", + subnetSetCR: &v1alpha1.SubnetSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "subnetset2", + Namespace: "default", + }, + }, + expSubnetConnectionBindingMaps: []v1alpha1.SubnetConnectionBindingMap{}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + actBindings := r.getSubnetBindingCRsBySubnetSet(ctx, tc.subnetSetCR) + assert.ElementsMatch(t, tc.expSubnetConnectionBindingMaps, actBindings) + }) + } +} + +func TestGetNSXSubnetBindingsBySubnet(t *testing.T) { + for _, tc := range []struct { + name string + subnetsetCRUID string + patches func(r *SubnetSetReconciler) *gomonkey.Patches + expSubnetConnectionBindingMaps []*v1alpha1.SubnetConnectionBindingMap + }{ + { + name: "No NSX VpcSubnet exists for the Subnet CR", + subnetsetCRUID: "uuid1", + patches: func(r *SubnetSetReconciler) *gomonkey.Patches { + patch := gomonkey.ApplyMethod(reflect.TypeOf(r.SubnetService), "ListSubnetCreatedBySubnetSet", func(_ *subnet.SubnetService, _ string) []*model.VpcSubnet { + return []*model.VpcSubnet{} + }) + return patch + }, + expSubnetConnectionBindingMaps: nil, + }, { + name: "No NSX SubnetConnectionBindingMap created for VpcSubnet", + subnetsetCRUID: "uuid1", + patches: func(r *SubnetSetReconciler) *gomonkey.Patches { + patch := gomonkey.ApplyMethod(reflect.TypeOf(r.SubnetService), "ListSubnetCreatedBySubnetSet", func(_ *subnet.SubnetService, _ string) []*model.VpcSubnet { + return []*model.VpcSubnet{ + {Id: common.String("id1")}, + } + }) + patch.ApplyMethod(reflect.TypeOf(r.BindingService), "GetSubnetConnectionBindingMapCRsBySubnet", func(_ *subnetbinding.BindingService, _ *model.VpcSubnet) []*v1alpha1.SubnetConnectionBindingMap { + return []*v1alpha1.SubnetConnectionBindingMap{} + }) + return patch + }, + expSubnetConnectionBindingMaps: nil, + }, { + name: "Partial of VpcSubnets are associated with the NSX SubnetConnectionBindingMap", + subnetsetCRUID: "uuid1", + patches: func(r *SubnetSetReconciler) *gomonkey.Patches { + patch := gomonkey.ApplyMethod(reflect.TypeOf(r.SubnetService), "ListSubnetCreatedBySubnetSet", func(_ *subnet.SubnetService, _ string) []*model.VpcSubnet { + return []*model.VpcSubnet{ + {Id: common.String("id1")}, + {Id: common.String("id2")}, + } + }) + patch.ApplyMethod(reflect.TypeOf(r.BindingService), "GetSubnetConnectionBindingMapCRsBySubnet", func(_ *subnetbinding.BindingService, subnet *model.VpcSubnet) []*v1alpha1.SubnetConnectionBindingMap { + if *subnet.Id == "id1" { + return []*v1alpha1.SubnetConnectionBindingMap{} + } + return []*v1alpha1.SubnetConnectionBindingMap{bm1} + }) + return patch + }, + expSubnetConnectionBindingMaps: []*v1alpha1.SubnetConnectionBindingMap{bm1}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + r := &SubnetSetReconciler{ + SubnetService: &subnet.SubnetService{}, + BindingService: &subnetbinding.BindingService{}, + } + patches := tc.patches(r) + defer patches.Reset() + + actBindings := r.getNSXSubnetBindingsBySubnetSet(tc.subnetsetCRUID) + assert.ElementsMatch(t, tc.expSubnetConnectionBindingMaps, actBindings) + }) + } +} diff --git a/pkg/controllers/subnetset/subnetset_controller_test.go b/pkg/controllers/subnetset/subnetset_controller_test.go index 2f9433811..a1ee6a4ed 100644 --- a/pkg/controllers/subnetset/subnetset_controller_test.go +++ b/pkg/controllers/subnetset/subnetset_controller_test.go @@ -13,6 +13,7 @@ import ( "github.com/agiledragon/gomonkey/v2" "github.com/google/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/vmware/vsphere-automation-sdk-go/services/nsxt/model" v12 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -224,8 +225,8 @@ func TestReconcile(t *testing.T) { patches := gomonkey.ApplyMethod(reflect.TypeOf(r.VPCService), "GetVPCNetworkConfigByNamespace", func(_ *vpc.VPCService, ns string) *common.VPCNetworkConfigInfo { return vpcnetworkInfo }) - patches.ApplyPrivateMethod(reflect.TypeOf(r), "getSubnetBindingCRsBySubnetSet", func(_ *SubnetSetReconciler, _ context.Context, _ *v1alpha1.SubnetSet) []v1alpha1.SubnetConnectionBindingMap { - return []v1alpha1.SubnetConnectionBindingMap{} + patches.ApplyPrivateMethod(reflect.TypeOf(r), "getSubnetBindingCRsBySubnetSet", func(_ *SubnetSetReconciler, _ context.Context, _ *v1alpha1.SubnetSet) []*v1alpha1.SubnetConnectionBindingMap { + return []*v1alpha1.SubnetConnectionBindingMap{} }) patches.ApplyMethod(reflect.TypeOf(r.SubnetService.SubnetStore), "GetByIndex", func(_ *subnet.SubnetStore, key string, value string) []*model.VpcSubnet { @@ -313,6 +314,199 @@ func TestReconcile(t *testing.T) { } } +func TestReconcileWithSubnetConnectionBindingMaps(t *testing.T) { + name := "subnetset" + ns := "ns1" + testSubnetSet1 := &v1alpha1.SubnetSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + }, + Spec: v1alpha1.SubnetSetSpec{ + AccessMode: v1alpha1.AccessMode(v1alpha1.AccessModePrivate), + IPv4SubnetSize: 16, + }, + } + testSubnetSet2 := &v1alpha1.SubnetSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + Finalizers: []string{ + common.SubnetSetFinalizerName, + }, + }, + Spec: v1alpha1.SubnetSetSpec{ + AccessMode: v1alpha1.AccessMode(v1alpha1.AccessModePrivate), + IPv4SubnetSize: 16, + }, + } + deleteTime := metav1.Now() + testSubnetSet3 := &v1alpha1.SubnetSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + Finalizers: []string{ + common.SubnetSetFinalizerName, + }, + DeletionTimestamp: &deleteTime, + }, + } + for _, tc := range []struct { + name string + existingSubnetSet *v1alpha1.SubnetSet + patches func(t *testing.T, r *SubnetSetReconciler) *gomonkey.Patches + expectErrStr string + expectRes ctrl.Result + }{ + { + name: "Successfully add finalizer after a SubnetSet is used by SubnetConnectionBindingMap", + existingSubnetSet: testSubnetSet1, + patches: func(t *testing.T, r *SubnetSetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyPrivateMethod(reflect.TypeOf(r), "getSubnetBindingCRsBySubnetSet", func(_ *SubnetSetReconciler, _ context.Context, _ *v1alpha1.SubnetSet) []v1alpha1.SubnetConnectionBindingMap { + return []v1alpha1.SubnetConnectionBindingMap{{ObjectMeta: metav1.ObjectMeta{Name: "binding1", Namespace: ns}}} + }) + patches.ApplyMethod(reflect.TypeOf(r.Client), "Update", func(_ client.Client, _ context.Context, obj client.Object, opts ...client.UpdateOption) error { + return nil + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetService.SubnetStore), "GetByIndex", func(_ *subnet.SubnetStore, _ string, _ string) []*model.VpcSubnet { + return []*model.VpcSubnet{} + }) + return patches + }, + expectRes: ctrl.Result{}, + }, { + name: "Failed to add finalizer after a SubnetSet is used by SubnetConnectionBindingMap", + existingSubnetSet: testSubnetSet1, + patches: func(t *testing.T, r *SubnetSetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyPrivateMethod(reflect.TypeOf(r), "getSubnetBindingCRsBySubnetSet", func(_ *SubnetSetReconciler, _ context.Context, _ *v1alpha1.SubnetSet) []v1alpha1.SubnetConnectionBindingMap { + return []v1alpha1.SubnetConnectionBindingMap{{ObjectMeta: metav1.ObjectMeta{Name: "binding1", Namespace: ns}}} + }) + patches.ApplyMethod(reflect.TypeOf(r.Client), "Update", func(_ client.Client, _ context.Context, obj client.Object, opts ...client.UpdateOption) error { + return fmt.Errorf("failed to update CR") + }) + patches.ApplyFunc(updateSubnetSetStatusConditions, func(_ client.Client, _ context.Context, _ *v1alpha1.SubnetSet, newConditions []v1alpha1.Condition) { + require.Equal(t, 1, len(newConditions)) + cond := newConditions[0] + assert.Equal(t, "Failed to add the finalizer on SubnetSet for the dependency by SubnetConnectionBindingMap binding1", cond.Message) + }) + return patches + }, + expectRes: ctlcommon.ResultRequeue, + expectErrStr: "failed to update CR", + }, { + name: "Not add duplicated finalizer after a SubnetSet is used by SubnetConnectionBindingMap", + existingSubnetSet: testSubnetSet2, + patches: func(t *testing.T, r *SubnetSetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyPrivateMethod(reflect.TypeOf(r), "getSubnetBindingCRsBySubnetSet", func(_ *SubnetSetReconciler, _ context.Context, _ *v1alpha1.SubnetSet) []v1alpha1.SubnetConnectionBindingMap { + return []v1alpha1.SubnetConnectionBindingMap{{ObjectMeta: metav1.ObjectMeta{Name: "binding1", Namespace: ns}}} + }) + patches.ApplyMethod(reflect.TypeOf(r.Client), "Update", func(_ client.Client, _ context.Context, obj client.Object, opts ...client.UpdateOption) error { + assert.FailNow(t, "Should not update SubnetSet CR finalizer") + return nil + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetService.SubnetStore), "GetByIndex", func(_ *subnet.SubnetStore, _ string, _ string) []*model.VpcSubnet { + return []*model.VpcSubnet{} + }) + patches.ApplyFunc(setSubnetSetReadyStatusTrue, func(_ client.Client, _ context.Context, _ client.Object, _ metav1.Time, _ ...interface{}) { + }) + return patches + }, + expectRes: ctrl.Result{}, + }, { + name: "Successfully remove finalizer after a Subnet is not used by any SubnetConnectionBindingMap", + existingSubnetSet: testSubnetSet2, + patches: func(t *testing.T, r *SubnetSetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyPrivateMethod(reflect.TypeOf(r), "getSubnetBindingCRsBySubnetSet", func(_ *SubnetSetReconciler, _ context.Context, _ *v1alpha1.SubnetSet) []v1alpha1.SubnetConnectionBindingMap { + return []v1alpha1.SubnetConnectionBindingMap{} + }) + patches.ApplyMethod(reflect.TypeOf(r.Client), "Update", func(_ client.Client, _ context.Context, obj client.Object, opts ...client.UpdateOption) error { + return nil + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetService.SubnetStore), "GetByIndex", func(_ *subnet.SubnetStore, _ string, _ string) []*model.VpcSubnet { + return []*model.VpcSubnet{} + }) + return patches + }, + expectRes: ctrl.Result{}, + }, { + name: "Failed to remove finalizer after a Subnet is not used by any SubnetConnectionBindingMap", + existingSubnetSet: testSubnetSet2, + patches: func(t *testing.T, r *SubnetSetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyPrivateMethod(reflect.TypeOf(r), "getSubnetBindingCRsBySubnetSet", func(_ *SubnetSetReconciler, _ context.Context, _ *v1alpha1.SubnetSet) []v1alpha1.SubnetConnectionBindingMap { + return []v1alpha1.SubnetConnectionBindingMap{} + }) + patches.ApplyMethod(reflect.TypeOf(r.Client), "Update", func(_ client.Client, _ context.Context, _ client.Object, opts ...client.UpdateOption) error { + return fmt.Errorf("failed to update CR") + }) + patches.ApplyFunc(updateSubnetSetStatusConditions, func(_ client.Client, _ context.Context, _ *v1alpha1.SubnetSet, newConditions []v1alpha1.Condition) { + require.Equal(t, 1, len(newConditions)) + cond := newConditions[0] + assert.Equal(t, "Failed to remove the finalizer on SubnetSet when there is no reference by SubnetConnectionBindingMaps", cond.Message) + }) + return patches + }, + expectRes: ctlcommon.ResultRequeue, + expectErrStr: "failed to update CR", + }, { + name: "Not update finalizers if a SubnetSet is not used by any SubnetConnectionBindingMap", + existingSubnetSet: testSubnetSet1, + patches: func(t *testing.T, r *SubnetSetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyPrivateMethod(reflect.TypeOf(r), "getSubnetBindingCRsBySubnetSet", func(_ *SubnetSetReconciler, _ context.Context, _ *v1alpha1.SubnetSet) []v1alpha1.SubnetConnectionBindingMap { + return []v1alpha1.SubnetConnectionBindingMap{} + }) + patches.ApplyMethod(reflect.TypeOf(r.Client), "Update", func(_ client.Client, _ context.Context, obj client.Object, opts ...client.UpdateOption) error { + assert.FailNow(t, "Should not update SubnetSet CR finalizer") + return nil + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetService.SubnetStore), "GetByIndex", func(_ *subnet.SubnetStore, _ string, _ string) []*model.VpcSubnet { + return []*model.VpcSubnet{} + }) + patches.ApplyFunc(setSubnetSetReadyStatusTrue, func(_ client.Client, _ context.Context, _ client.Object, _ metav1.Time, _ ...interface{}) { + }) + return patches + }, + expectRes: ctrl.Result{}, + }, { + name: "Delete a SubnetSet is not allowed if it is used by SubnetConnectionBindingMap", + existingSubnetSet: testSubnetSet3, + patches: func(t *testing.T, r *SubnetSetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyPrivateMethod(reflect.TypeOf(r), "getSubnetBindingCRsBySubnetSet", func(_ *SubnetSetReconciler, _ context.Context, _ *v1alpha1.SubnetSet) []v1alpha1.SubnetConnectionBindingMap { + return []v1alpha1.SubnetConnectionBindingMap{} + }) + patches.ApplyPrivateMethod(reflect.TypeOf(r), "getNSXSubnetBindingsBySubnetSet", func(_ *SubnetSetReconciler, _ string) []*v1alpha1.SubnetConnectionBindingMap { + return []*v1alpha1.SubnetConnectionBindingMap{{ObjectMeta: metav1.ObjectMeta{Name: "binding1", Namespace: ns}}} + }) + patches.ApplyPrivateMethod(reflect.TypeOf(r), "setSubnetDeletionFailedStatus", func(_ *SubnetSetReconciler, _ context.Context, _ *v1alpha1.Subnet, _ metav1.Time, msg string, reason string) { + assert.Equal(t, "SubnetSet is used by SubnetConnectionBindingMap binding1 and not able to delete", msg) + assert.Equal(t, "SubnetSetInUse", reason) + }) + return patches + }, + expectRes: ResultRequeue, + expectErrStr: "failed to delete SubnetSet CR ns1/subnetset", + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := context.TODO() + req := ctrl.Request{NamespacedName: types.NamespacedName{Name: name, Namespace: ns}} + r := createFakeSubnetSetReconciler([]client.Object{tc.existingSubnetSet}) + if tc.patches != nil { + patches := tc.patches(t, r) + defer patches.Reset() + } + + res, err := r.Reconcile(ctx, req) + + if tc.expectErrStr != "" { + assert.EqualError(t, err, tc.expectErrStr) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tc.expectRes, res) + }) + } +} + // Test Reconcile - SubnetSet Deletion func TestReconcile_DeleteSubnetSet(t *testing.T) { subnetSetName := "test-subnetset" @@ -785,3 +979,186 @@ func TestStartSubnetSetController(t *testing.T) { }) } } + +func TestDeleteSubnets(t *testing.T) { + nsxSubnets := []*model.VpcSubnet{{ + Id: common.String("net1"), + Path: common.String("subnet1-path"), + }, { + Id: common.String("net2"), + Path: common.String("subnet2-path"), + }} + testLock := &sync.Mutex{} + for _, tc := range []struct { + name string + nsxSubnets []*model.VpcSubnet + deleteBindingMaps bool + patches func(t *testing.T, r *SubnetSetReconciler) *gomonkey.Patches + expHasStalePort bool + expErrStr string + }{ + { + name: "No NSX subnets found", + nsxSubnets: []*model.VpcSubnet{}, + deleteBindingMaps: false, + expHasStalePort: false, + expErrStr: "", + }, { + name: "One of the NSX subnet has stale ports", + nsxSubnets: nsxSubnets, + deleteBindingMaps: false, + patches: func(t *testing.T, r *SubnetSetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyFunc(ctlcommon.LockSubnetSet, func(uuid types.UID) *sync.Mutex { + testLock.Lock() + return testLock + }) + patches.ApplyFunc(ctlcommon.UnlockSubnetSet, func(_ types.UID, subnetSetLock *sync.Mutex) { + testLock.Unlock() + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetPortService), "IsEmptySubnet", func(_ *subnetport.SubnetPortService, id string, path string) bool { + if id == "net1" { + return false + } + return true + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "DeleteSubnet", func(_ *subnet.SubnetService, nsxSubnet model.VpcSubnet) error { + if *nsxSubnet.Id == "net1" { + require.Fail(t, "SubnetService.DeleteSubnet should not be called if stale ports exist") + } + return nil + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetPortService), "DeletePortCount", func(_ *subnetport.SubnetPortService, _ string) {}) + return patches + }, + expHasStalePort: true, + expErrStr: "", + }, { + name: "Failed to delete NSX subnets", + nsxSubnets: nsxSubnets, + deleteBindingMaps: false, + patches: func(t *testing.T, r *SubnetSetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyFunc(ctlcommon.LockSubnetSet, func(uuid types.UID) *sync.Mutex { + testLock.Lock() + return testLock + }) + patches.ApplyFunc(ctlcommon.UnlockSubnetSet, func(uuid types.UID, subnetSetLock *sync.Mutex) { + testLock.Unlock() + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetPortService), "IsEmptySubnet", func(_ *subnetport.SubnetPortService, id string, path string) bool { + return true + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "DeleteSubnet", func(_ *subnet.SubnetService, nsxSubnet model.VpcSubnet) error { + if *nsxSubnet.Id == "net1" { + return fmt.Errorf("net1 deletion failed") + } + return nil + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetPortService), "DeletePortCount", func(_ *subnetport.SubnetPortService, _ string) {}) + return patches + }, + expHasStalePort: false, + expErrStr: "multiple errors occurred while deleting Subnets: [failed to delete NSX Subnet/net1: net1 deletion failed]", + }, { + name: "Succeeded to delete NSX subnets", + nsxSubnets: nsxSubnets, + deleteBindingMaps: false, + patches: func(t *testing.T, r *SubnetSetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyFunc(ctlcommon.LockSubnetSet, func(uuid types.UID) *sync.Mutex { + testLock.Lock() + return testLock + }) + patches.ApplyFunc(ctlcommon.UnlockSubnetSet, func(uuid types.UID, subnetSetLock *sync.Mutex) { + testLock.Unlock() + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetPortService), "IsEmptySubnet", func(_ *subnetport.SubnetPortService, id string, path string) bool { + return true + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "DeleteSubnet", func(_ *subnet.SubnetService, nsxSubnet model.VpcSubnet) error { + return nil + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetPortService), "DeletePortCount", func(_ *subnetport.SubnetPortService, _ string) {}) + return patches + }, + expHasStalePort: false, + expErrStr: "", + }, { + name: "Failed to delete NSX subnet connection binding maps", + nsxSubnets: nsxSubnets, + deleteBindingMaps: true, + patches: func(t *testing.T, r *SubnetSetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyFunc(ctlcommon.LockSubnetSet, func(uuid types.UID) *sync.Mutex { + testLock.Lock() + return testLock + }) + patches.ApplyFunc(ctlcommon.UnlockSubnetSet, func(uuid types.UID, subnetSetLock *sync.Mutex) { + testLock.Unlock() + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetPortService), "IsEmptySubnet", func(_ *subnetport.SubnetPortService, id string, path string) bool { + return true + }) + patches.ApplyMethod(reflect.TypeOf(r.BindingService), "DeleteSubnetConnectionBindingMapsByParentSubnet", func(_ *subnetbinding.BindingService, parentSubnet *model.VpcSubnet) error { + if *parentSubnet.Id == "net1" { + return fmt.Errorf("binding maps deletion failed") + } + return nil + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "DeleteSubnet", func(_ *subnet.SubnetService, nsxSubnet model.VpcSubnet) error { + if *nsxSubnet.Id == "net1" { + require.Fail(t, "SubnetService.DeleteSubnet should not be called if binding maps are failed to delete") + } + return nil + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetPortService), "DeletePortCount", func(_ *subnetport.SubnetPortService, _ string) {}) + return patches + }, + expHasStalePort: false, + expErrStr: "multiple errors occurred while deleting Subnets: [failed to delete NSX SubnetConnectionBindingMaps connected to NSX Subnet/net1: binding maps deletion failed]", + }, { + name: "Succeeded to delete NSX subnet and connection binding maps", + nsxSubnets: nsxSubnets, + deleteBindingMaps: true, + patches: func(t *testing.T, r *SubnetSetReconciler) *gomonkey.Patches { + patches := gomonkey.ApplyFunc(ctlcommon.LockSubnetSet, func(uuid types.UID) *sync.Mutex { + testLock.Lock() + return testLock + }) + patches.ApplyFunc(ctlcommon.UnlockSubnetSet, func(uuid types.UID, subnetSetLock *sync.Mutex) { + testLock.Unlock() + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetPortService), "IsEmptySubnet", func(_ *subnetport.SubnetPortService, id string, path string) bool { + return true + }) + patches.ApplyMethod(reflect.TypeOf(r.BindingService), "DeleteSubnetConnectionBindingMapsByParentSubnet", func(_ *subnetbinding.BindingService, parentSubnet *model.VpcSubnet) error { + return nil + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetService), "DeleteSubnet", func(_ *subnet.SubnetService, nsxSubnet model.VpcSubnet) error { + return nil + }) + patches.ApplyMethod(reflect.TypeOf(r.SubnetPortService), "DeletePortCount", func(_ *subnetport.SubnetPortService, _ string) {}) + return patches + }, + expHasStalePort: false, + expErrStr: "", + }, + } { + t.Run(tc.name, func(t *testing.T) { + r := &SubnetSetReconciler{ + SubnetService: &subnet.SubnetService{}, + SubnetPortService: &subnetport.SubnetPortService{}, + BindingService: &subnetbinding.BindingService{}, + } + if tc.patches != nil { + patches := tc.patches(t, r) + defer patches.Reset() + } + + hasPorts, err := r.deleteSubnets(tc.nsxSubnets, tc.deleteBindingMaps) + if tc.expErrStr != "" { + require.EqualError(t, err, tc.expErrStr) + } else { + require.NoError(t, err) + } + require.Equal(t, tc.expHasStalePort, hasPorts) + }) + } +} diff --git a/pkg/nsx/services/subnetbinding/subnetbinding_test.go b/pkg/nsx/services/subnetbinding/subnetbinding_test.go index 995973776..1271187ee 100644 --- a/pkg/nsx/services/subnetbinding/subnetbinding_test.go +++ b/pkg/nsx/services/subnetbinding/subnetbinding_test.go @@ -3,8 +3,10 @@ package subnetbinding import ( "context" "fmt" + "reflect" "testing" + "github.com/agiledragon/gomonkey/v2" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -499,3 +501,48 @@ func TestDeleteSubnetConnectionBindingMaps(t *testing.T) { }) } } + +func TestDeleteSubnetConnectionBindingMaps_WithPaging(t *testing.T) { + for _, tc := range []struct { + name string + bindingCount int + expPages int + }{ + { + name: "no paging on the requests", + bindingCount: 50, + expPages: 1, + }, { + name: "paging on the requests", + bindingCount: 1501, + expPages: 2, + }, + } { + t.Run(tc.name, func(t *testing.T) { + actPages := 0 + targetBindingMaps := make([]*model.SubnetConnectionBindingMap, tc.bindingCount) + expDeletedBindings := make([]string, tc.bindingCount) + for i := 0; i < tc.bindingCount; i++ { + idString := fmt.Sprintf("id-%d", i) + expDeletedBindings[i] = idString + targetBindingMaps[i] = &model.SubnetConnectionBindingMap{ + Id: common.String(idString), + } + } + actDeletedBindings := make([]string, 0) + svc := &BindingService{} + patches := gomonkey.ApplyPrivateMethod(reflect.TypeOf(svc), "hDeleteSubnetConnectionBindingMap", func(_ *BindingService, bindingMaps []*model.SubnetConnectionBindingMap) error { + for _, bm := range bindingMaps { + actDeletedBindings = append(actDeletedBindings, *bm.Id) + } + actPages += 1 + return nil + }) + defer patches.Reset() + err := svc.deleteSubnetConnectionBindingMaps(targetBindingMaps) + require.NoError(t, err) + assert.ElementsMatch(t, expDeletedBindings, actDeletedBindings) + assert.Equal(t, tc.expPages, actPages) + }) + } +}