sigs.k8s.io/cluster-api-provider-azure@v1.14.3/azure/services/aso/service_test.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package aso
    18  
    19  import (
    20  	"context"
    21  	"testing"
    22  
    23  	asoresourcesv1 "github.com/Azure/azure-service-operator/v2/api/resources/v1api20200601"
    24  	. "github.com/onsi/gomega"
    25  	"github.com/pkg/errors"
    26  	"go.uber.org/mock/gomock"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    30  	"sigs.k8s.io/cluster-api-provider-azure/azure"
    31  	"sigs.k8s.io/cluster-api-provider-azure/azure/mock_azure"
    32  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/aso/mock_aso"
    33  	gomockinternal "sigs.k8s.io/cluster-api-provider-azure/internal/test/matchers/gomock"
    34  	reconcilerutils "sigs.k8s.io/cluster-api-provider-azure/util/reconciler"
    35  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    36  	"sigs.k8s.io/controller-runtime/pkg/client"
    37  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    38  )
    39  
    40  const (
    41  	serviceName   = "test"
    42  	conditionType = clusterv1.ConditionType("Test")
    43  )
    44  
    45  func TestServiceReconcile(t *testing.T) {
    46  	t.Run("no specs", func(t *testing.T) {
    47  		g := NewGomegaWithT(t)
    48  
    49  		mockCtrl := gomock.NewController(t)
    50  		postReconcileErr := errors.New("PostReconcileHook error")
    51  		scope := mock_aso.NewMockScope(mockCtrl)
    52  		scope.EXPECT().UpdatePutStatus(conditionType, serviceName, postReconcileErr)
    53  		reconciler := mock_aso.NewMockReconciler[*asoresourcesv1.ResourceGroup](mockCtrl)
    54  		scope.EXPECT().DefaultedAzureServiceReconcileTimeout().Return(reconcilerutils.DefaultAzureServiceReconcileTimeout)
    55  
    56  		s := &Service[*asoresourcesv1.ResourceGroup, *mock_aso.MockScope]{
    57  			Reconciler:    reconciler,
    58  			Scope:         scope,
    59  			Specs:         nil,
    60  			name:          serviceName,
    61  			ConditionType: conditionType,
    62  			PostReconcileHook: func(_ context.Context, _ *mock_aso.MockScope, _ error) error {
    63  				return postReconcileErr
    64  			},
    65  		}
    66  
    67  		err := s.Reconcile(context.Background())
    68  		g.Expect(err).To(MatchError(postReconcileErr))
    69  	})
    70  
    71  	t.Run("CreateOrUpdateResource returns error", func(t *testing.T) {
    72  		g := NewGomegaWithT(t)
    73  
    74  		mockCtrl := gomock.NewController(t)
    75  
    76  		scope := mock_aso.NewMockScope(mockCtrl)
    77  		specs := []azure.ASOResourceSpecGetter[*asoresourcesv1.ResourceGroup]{
    78  			mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl),
    79  		}
    80  
    81  		reconcileErr := errors.New("CreateOrUpdateResource error")
    82  		reconciler := mock_aso.NewMockReconciler[*asoresourcesv1.ResourceGroup](mockCtrl)
    83  		reconciler.EXPECT().CreateOrUpdateResource(gomockinternal.AContext(), specs[0], serviceName).Return(nil, reconcileErr)
    84  		scope.EXPECT().UpdatePutStatus(conditionType, serviceName, reconcileErr)
    85  		scope.EXPECT().DefaultedAzureServiceReconcileTimeout().Return(reconcilerutils.DefaultAzureServiceReconcileTimeout)
    86  
    87  		s := &Service[*asoresourcesv1.ResourceGroup, *mock_aso.MockScope]{
    88  			Reconciler:    reconciler,
    89  			Scope:         scope,
    90  			Specs:         specs,
    91  			name:          serviceName,
    92  			ConditionType: conditionType,
    93  		}
    94  
    95  		err := s.Reconcile(context.Background())
    96  		g.Expect(err).To(MatchError(reconcileErr))
    97  	})
    98  
    99  	t.Run("CreateOrUpdateResource succeeds for all resources", func(t *testing.T) {
   100  		g := NewGomegaWithT(t)
   101  
   102  		mockCtrl := gomock.NewController(t)
   103  
   104  		scope := mock_aso.NewMockScope(mockCtrl)
   105  		specs := []azure.ASOResourceSpecGetter[*asoresourcesv1.ResourceGroup]{
   106  			mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl),
   107  			mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl),
   108  			mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl),
   109  		}
   110  
   111  		reconciler := mock_aso.NewMockReconciler[*asoresourcesv1.ResourceGroup](mockCtrl)
   112  		reconciler.EXPECT().CreateOrUpdateResource(gomockinternal.AContext(), specs[0], serviceName).Return(nil, nil)
   113  		reconciler.EXPECT().CreateOrUpdateResource(gomockinternal.AContext(), specs[1], serviceName).Return(nil, nil)
   114  		reconciler.EXPECT().CreateOrUpdateResource(gomockinternal.AContext(), specs[2], serviceName).Return(nil, nil)
   115  		scope.EXPECT().UpdatePutStatus(conditionType, serviceName, nil)
   116  		scope.EXPECT().DefaultedAzureServiceReconcileTimeout().Return(reconcilerutils.DefaultAzureServiceReconcileTimeout)
   117  
   118  		s := &Service[*asoresourcesv1.ResourceGroup, *mock_aso.MockScope]{
   119  			Reconciler:    reconciler,
   120  			Scope:         scope,
   121  			Specs:         specs,
   122  			name:          serviceName,
   123  			ConditionType: conditionType,
   124  		}
   125  
   126  		err := s.Reconcile(context.Background())
   127  		g.Expect(err).NotTo(HaveOccurred())
   128  	})
   129  
   130  	t.Run("CreateOrUpdateResource returns not done", func(t *testing.T) {
   131  		g := NewGomegaWithT(t)
   132  
   133  		mockCtrl := gomock.NewController(t)
   134  
   135  		scope := mock_aso.NewMockScope(mockCtrl)
   136  		specs := []azure.ASOResourceSpecGetter[*asoresourcesv1.ResourceGroup]{
   137  			mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl),
   138  			mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl),
   139  			mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl),
   140  		}
   141  
   142  		reconcileErr := azure.NewOperationNotDoneError(&infrav1.Future{})
   143  		reconciler := mock_aso.NewMockReconciler[*asoresourcesv1.ResourceGroup](mockCtrl)
   144  		reconciler.EXPECT().CreateOrUpdateResource(gomockinternal.AContext(), specs[0], serviceName).Return(nil, nil)
   145  		reconciler.EXPECT().CreateOrUpdateResource(gomockinternal.AContext(), specs[1], serviceName).Return(nil, reconcileErr)
   146  		reconciler.EXPECT().CreateOrUpdateResource(gomockinternal.AContext(), specs[2], serviceName).Return(nil, nil)
   147  		scope.EXPECT().UpdatePutStatus(conditionType, serviceName, reconcileErr)
   148  		scope.EXPECT().DefaultedAzureServiceReconcileTimeout().Return(reconcilerutils.DefaultAzureServiceReconcileTimeout)
   149  
   150  		s := &Service[*asoresourcesv1.ResourceGroup, *mock_aso.MockScope]{
   151  			Reconciler:    reconciler,
   152  			Scope:         scope,
   153  			Specs:         specs,
   154  			name:          serviceName,
   155  			ConditionType: conditionType,
   156  		}
   157  
   158  		err := s.Reconcile(context.Background())
   159  		g.Expect(azure.IsOperationNotDoneError(err)).To(BeTrue())
   160  	})
   161  
   162  	t.Run("CreateOrUpdateResource returns not done and another error", func(t *testing.T) {
   163  		g := NewGomegaWithT(t)
   164  
   165  		mockCtrl := gomock.NewController(t)
   166  
   167  		scope := mock_aso.NewMockScope(mockCtrl)
   168  		specs := []azure.ASOResourceSpecGetter[*asoresourcesv1.ResourceGroup]{
   169  			mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl),
   170  			mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl),
   171  			mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl),
   172  		}
   173  
   174  		reconcileErr := errors.New("non-not done error")
   175  		reconciler := mock_aso.NewMockReconciler[*asoresourcesv1.ResourceGroup](mockCtrl)
   176  		reconciler.EXPECT().CreateOrUpdateResource(gomockinternal.AContext(), specs[0], serviceName).Return(nil, azure.NewOperationNotDoneError(&infrav1.Future{}))
   177  		reconciler.EXPECT().CreateOrUpdateResource(gomockinternal.AContext(), specs[1], serviceName).Return(nil, reconcileErr)
   178  		reconciler.EXPECT().CreateOrUpdateResource(gomockinternal.AContext(), specs[2], serviceName).Return(nil, azure.NewOperationNotDoneError(&infrav1.Future{}))
   179  		scope.EXPECT().UpdatePutStatus(conditionType, serviceName, reconcileErr)
   180  		scope.EXPECT().DefaultedAzureServiceReconcileTimeout().Return(reconcilerutils.DefaultAzureServiceReconcileTimeout)
   181  
   182  		s := &Service[*asoresourcesv1.ResourceGroup, *mock_aso.MockScope]{
   183  			Reconciler:    reconciler,
   184  			Scope:         scope,
   185  			Specs:         specs,
   186  			name:          serviceName,
   187  			ConditionType: conditionType,
   188  		}
   189  
   190  		err := s.Reconcile(context.Background())
   191  		g.Expect(err).To(MatchError(reconcileErr))
   192  	})
   193  
   194  	t.Run("CreateOrUpdateResource returns error and runs PostCreateOrUpdateResourceHook and PostReconcileHook", func(t *testing.T) {
   195  		g := NewGomegaWithT(t)
   196  
   197  		mockCtrl := gomock.NewController(t)
   198  
   199  		scope := mock_aso.NewMockScope(mockCtrl)
   200  		specs := []azure.ASOResourceSpecGetter[*asoresourcesv1.ResourceGroup]{
   201  			mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl),
   202  		}
   203  
   204  		reconcileErr := errors.New("CreateOrUpdateResource error")
   205  		postResourceErr := errors.New("PostCreateOrUpdateResource error")
   206  		postReconcileErr := errors.New("PostReconcile error")
   207  		reconciler := mock_aso.NewMockReconciler[*asoresourcesv1.ResourceGroup](mockCtrl)
   208  		reconciler.EXPECT().CreateOrUpdateResource(gomockinternal.AContext(), specs[0], serviceName).Return(&asoresourcesv1.ResourceGroup{
   209  			ObjectMeta: metav1.ObjectMeta{
   210  				Name: "a very special name",
   211  			},
   212  		}, reconcileErr)
   213  		scope.EXPECT().UpdatePutStatus(conditionType, serviceName, postReconcileErr)
   214  		scope.EXPECT().DefaultedAzureServiceReconcileTimeout().Return(reconcilerutils.DefaultAzureServiceReconcileTimeout)
   215  
   216  		s := &Service[*asoresourcesv1.ResourceGroup, *mock_aso.MockScope]{
   217  			Reconciler:    reconciler,
   218  			Scope:         scope,
   219  			Specs:         specs,
   220  			name:          serviceName,
   221  			ConditionType: conditionType,
   222  			PostCreateOrUpdateResourceHook: func(_ context.Context, scopeParam *mock_aso.MockScope, result *asoresourcesv1.ResourceGroup, err error) error {
   223  				g.Expect(scopeParam).To(BeIdenticalTo(scope))
   224  				g.Expect(result.Name).To(Equal("a very special name"))
   225  				g.Expect(err).To(MatchError(reconcileErr))
   226  				return postResourceErr
   227  			},
   228  			PostReconcileHook: func(_ context.Context, scopeParam *mock_aso.MockScope, err error) error {
   229  				g.Expect(scopeParam).To(BeIdenticalTo(scope))
   230  				g.Expect(err).To(MatchError(postResourceErr))
   231  				return postReconcileErr
   232  			},
   233  		}
   234  
   235  		err := s.Reconcile(context.Background())
   236  		g.Expect(err).To(MatchError(postReconcileErr))
   237  	})
   238  
   239  	t.Run("stale resources are deleted", func(t *testing.T) {
   240  		g := NewGomegaWithT(t)
   241  
   242  		mockCtrl := gomock.NewController(t)
   243  
   244  		scope := mock_aso.NewMockScope(mockCtrl)
   245  		rg0 := &asoresourcesv1.ResourceGroup{ObjectMeta: metav1.ObjectMeta{Name: "spec0"}}
   246  		rg1 := &asoresourcesv1.ResourceGroup{ObjectMeta: metav1.ObjectMeta{Name: "spec1"}}
   247  		specs := []azure.ASOResourceSpecGetter[*asoresourcesv1.ResourceGroup]{
   248  			mockSpecExpectingResourceRef(mockCtrl, rg0),
   249  			mockSpecExpectingResourceRef(mockCtrl, rg1),
   250  		}
   251  
   252  		reconciler := mock_aso.NewMockReconciler[*asoresourcesv1.ResourceGroup](mockCtrl)
   253  		reconciler.EXPECT().CreateOrUpdateResource(gomockinternal.AContext(), specs[0], serviceName).Return(rg0, azure.NewOperationNotDoneError(&infrav1.Future{}))
   254  		reconciler.EXPECT().CreateOrUpdateResource(gomockinternal.AContext(), specs[1], serviceName).Return(rg1, nil)
   255  		scope.EXPECT().DefaultedAzureServiceReconcileTimeout().Return(reconcilerutils.DefaultAzureServiceReconcileTimeout)
   256  
   257  		sch := runtime.NewScheme()
   258  		g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed())
   259  		ctrlClient := fake.NewClientBuilder().
   260  			WithScheme(sch).
   261  			Build()
   262  
   263  		deleteErr := errors.New("delete error")
   264  
   265  		deleteMe := &asoresourcesv1.ResourceGroup{
   266  			ObjectMeta: metav1.ObjectMeta{
   267  				Name:      "delete-me",
   268  				Namespace: "namespace",
   269  			},
   270  		}
   271  		deleteMeToo := &asoresourcesv1.ResourceGroup{
   272  			ObjectMeta: metav1.ObjectMeta{
   273  				Name:      "delete-me-too",
   274  				Namespace: "namespace",
   275  			},
   276  		}
   277  
   278  		scope.EXPECT().GetClient().Return(ctrlClient).AnyTimes()
   279  		scope.EXPECT().ASOOwner().Return(&asoresourcesv1.ResourceGroup{}).AnyTimes()
   280  		list := func(_ context.Context, _ client.Client, _ ...client.ListOption) ([]*asoresourcesv1.ResourceGroup, error) {
   281  			return []*asoresourcesv1.ResourceGroup{
   282  				deleteMe,
   283  				rg0,
   284  				deleteMeToo,
   285  				rg1,
   286  			}, nil
   287  		}
   288  		reconciler.EXPECT().DeleteResource(gomockinternal.AContext(), deleteMe, serviceName).Return(deleteErr)
   289  		reconciler.EXPECT().DeleteResource(gomockinternal.AContext(), deleteMeToo, serviceName).Return(azure.NewOperationNotDoneError(&infrav1.Future{}))
   290  		scope.EXPECT().UpdatePutStatus(conditionType, serviceName, deleteErr)
   291  
   292  		s := &Service[*asoresourcesv1.ResourceGroup, *mock_aso.MockScope]{
   293  			Reconciler:    reconciler,
   294  			ListFunc:      list,
   295  			Scope:         scope,
   296  			Specs:         specs,
   297  			name:          serviceName,
   298  			ConditionType: conditionType,
   299  		}
   300  
   301  		err := s.Reconcile(context.Background())
   302  		g.Expect(err).To(MatchError(deleteErr))
   303  	})
   304  }
   305  
   306  func TestServiceDelete(t *testing.T) {
   307  	t.Run("no specs", func(t *testing.T) {
   308  		g := NewGomegaWithT(t)
   309  
   310  		mockCtrl := gomock.NewController(t)
   311  		scope := mock_aso.NewMockScope(mockCtrl)
   312  		reconciler := mock_aso.NewMockReconciler[*asoresourcesv1.ResourceGroup](mockCtrl)
   313  		scope.EXPECT().DefaultedAzureServiceReconcileTimeout().Return(reconcilerutils.DefaultAzureServiceReconcileTimeout)
   314  
   315  		s := &Service[*asoresourcesv1.ResourceGroup, *mock_aso.MockScope]{
   316  			Reconciler:    reconciler,
   317  			Scope:         scope,
   318  			Specs:         nil,
   319  			name:          serviceName,
   320  			ConditionType: conditionType,
   321  			PostDeleteHook: func(_ context.Context, _ *mock_aso.MockScope, _ error) error {
   322  				return errors.New("hook should not be called")
   323  			},
   324  		}
   325  
   326  		err := s.Delete(context.Background())
   327  		g.Expect(err).NotTo(HaveOccurred())
   328  	})
   329  
   330  	t.Run("DeleteResource returns error", func(t *testing.T) {
   331  		g := NewGomegaWithT(t)
   332  
   333  		mockCtrl := gomock.NewController(t)
   334  
   335  		scope := mock_aso.NewMockScope(mockCtrl)
   336  		specs := []azure.ASOResourceSpecGetter[*asoresourcesv1.ResourceGroup]{
   337  			mockSpecExpectingResourceRef(mockCtrl, &asoresourcesv1.ResourceGroup{}),
   338  		}
   339  
   340  		deleteErr := errors.New("DeleteResource error")
   341  		reconciler := mock_aso.NewMockReconciler[*asoresourcesv1.ResourceGroup](mockCtrl)
   342  		reconciler.EXPECT().DeleteResource(gomockinternal.AContext(), specs[0].ResourceRef(), serviceName).Return(deleteErr)
   343  		scope.EXPECT().UpdateDeleteStatus(conditionType, serviceName, deleteErr)
   344  		scope.EXPECT().DefaultedAzureServiceReconcileTimeout().Return(reconcilerutils.DefaultAzureServiceReconcileTimeout)
   345  
   346  		s := &Service[*asoresourcesv1.ResourceGroup, *mock_aso.MockScope]{
   347  			Reconciler:    reconciler,
   348  			Scope:         scope,
   349  			Specs:         specs,
   350  			name:          serviceName,
   351  			ConditionType: conditionType,
   352  		}
   353  
   354  		err := s.Delete(context.Background())
   355  		g.Expect(err).To(MatchError(deleteErr))
   356  	})
   357  
   358  	t.Run("DeleteResource succeeds for all resources", func(t *testing.T) {
   359  		g := NewGomegaWithT(t)
   360  
   361  		mockCtrl := gomock.NewController(t)
   362  
   363  		scope := mock_aso.NewMockScope(mockCtrl)
   364  		specs := []azure.ASOResourceSpecGetter[*asoresourcesv1.ResourceGroup]{
   365  			mockSpecExpectingResourceRef(mockCtrl, &asoresourcesv1.ResourceGroup{}),
   366  			mockSpecExpectingResourceRef(mockCtrl, &asoresourcesv1.ResourceGroup{}),
   367  			mockSpecExpectingResourceRef(mockCtrl, &asoresourcesv1.ResourceGroup{}),
   368  		}
   369  
   370  		reconciler := mock_aso.NewMockReconciler[*asoresourcesv1.ResourceGroup](mockCtrl)
   371  		reconciler.EXPECT().DeleteResource(gomockinternal.AContext(), specs[0].ResourceRef(), serviceName).Return(nil)
   372  		reconciler.EXPECT().DeleteResource(gomockinternal.AContext(), specs[1].ResourceRef(), serviceName).Return(nil)
   373  		reconciler.EXPECT().DeleteResource(gomockinternal.AContext(), specs[2].ResourceRef(), serviceName).Return(nil)
   374  		scope.EXPECT().UpdateDeleteStatus(conditionType, serviceName, nil)
   375  		scope.EXPECT().DefaultedAzureServiceReconcileTimeout().Return(reconcilerutils.DefaultAzureServiceReconcileTimeout)
   376  
   377  		s := &Service[*asoresourcesv1.ResourceGroup, *mock_aso.MockScope]{
   378  			Reconciler:    reconciler,
   379  			Scope:         scope,
   380  			Specs:         specs,
   381  			name:          serviceName,
   382  			ConditionType: conditionType,
   383  		}
   384  
   385  		err := s.Delete(context.Background())
   386  		g.Expect(err).NotTo(HaveOccurred())
   387  	})
   388  
   389  	t.Run("DeleteResource returns not done", func(t *testing.T) {
   390  		g := NewGomegaWithT(t)
   391  
   392  		mockCtrl := gomock.NewController(t)
   393  
   394  		scope := mock_aso.NewMockScope(mockCtrl)
   395  		specs := []azure.ASOResourceSpecGetter[*asoresourcesv1.ResourceGroup]{
   396  			mockSpecExpectingResourceRef(mockCtrl, &asoresourcesv1.ResourceGroup{}),
   397  			mockSpecExpectingResourceRef(mockCtrl, &asoresourcesv1.ResourceGroup{}),
   398  			mockSpecExpectingResourceRef(mockCtrl, &asoresourcesv1.ResourceGroup{}),
   399  		}
   400  
   401  		deleteErr := azure.NewOperationNotDoneError(&infrav1.Future{})
   402  		reconciler := mock_aso.NewMockReconciler[*asoresourcesv1.ResourceGroup](mockCtrl)
   403  		reconciler.EXPECT().DeleteResource(gomockinternal.AContext(), specs[0].ResourceRef(), serviceName).Return(nil)
   404  		reconciler.EXPECT().DeleteResource(gomockinternal.AContext(), specs[1].ResourceRef(), serviceName).Return(deleteErr)
   405  		reconciler.EXPECT().DeleteResource(gomockinternal.AContext(), specs[2].ResourceRef(), serviceName).Return(nil)
   406  		scope.EXPECT().UpdateDeleteStatus(conditionType, serviceName, deleteErr)
   407  		scope.EXPECT().DefaultedAzureServiceReconcileTimeout().Return(reconcilerutils.DefaultAzureServiceReconcileTimeout)
   408  
   409  		s := &Service[*asoresourcesv1.ResourceGroup, *mock_aso.MockScope]{
   410  			Reconciler:    reconciler,
   411  			Scope:         scope,
   412  			Specs:         specs,
   413  			name:          serviceName,
   414  			ConditionType: conditionType,
   415  		}
   416  
   417  		err := s.Delete(context.Background())
   418  		g.Expect(azure.IsOperationNotDoneError(err)).To(BeTrue())
   419  	})
   420  
   421  	t.Run("DeleteResource returns not done and another error", func(t *testing.T) {
   422  		g := NewGomegaWithT(t)
   423  
   424  		mockCtrl := gomock.NewController(t)
   425  
   426  		scope := mock_aso.NewMockScope(mockCtrl)
   427  		specs := []azure.ASOResourceSpecGetter[*asoresourcesv1.ResourceGroup]{
   428  			mockSpecExpectingResourceRef(mockCtrl, &asoresourcesv1.ResourceGroup{}),
   429  			mockSpecExpectingResourceRef(mockCtrl, &asoresourcesv1.ResourceGroup{}),
   430  			mockSpecExpectingResourceRef(mockCtrl, &asoresourcesv1.ResourceGroup{}),
   431  		}
   432  
   433  		deleteErr := errors.New("non-not done error")
   434  		reconciler := mock_aso.NewMockReconciler[*asoresourcesv1.ResourceGroup](mockCtrl)
   435  		reconciler.EXPECT().DeleteResource(gomockinternal.AContext(), specs[0].ResourceRef(), serviceName).Return(azure.NewOperationNotDoneError(&infrav1.Future{}))
   436  		reconciler.EXPECT().DeleteResource(gomockinternal.AContext(), specs[1].ResourceRef(), serviceName).Return(deleteErr)
   437  		reconciler.EXPECT().DeleteResource(gomockinternal.AContext(), specs[2].ResourceRef(), serviceName).Return(azure.NewOperationNotDoneError(&infrav1.Future{}))
   438  		scope.EXPECT().UpdateDeleteStatus(conditionType, serviceName, deleteErr)
   439  		scope.EXPECT().DefaultedAzureServiceReconcileTimeout().Return(reconcilerutils.DefaultAzureServiceReconcileTimeout)
   440  
   441  		s := &Service[*asoresourcesv1.ResourceGroup, *mock_aso.MockScope]{
   442  			Reconciler:    reconciler,
   443  			Scope:         scope,
   444  			Specs:         specs,
   445  			name:          serviceName,
   446  			ConditionType: conditionType,
   447  		}
   448  
   449  		err := s.Delete(context.Background())
   450  		g.Expect(err).To(MatchError(deleteErr))
   451  	})
   452  
   453  	t.Run("DeleteResource returns error and runs PostDeleteHook", func(t *testing.T) {
   454  		g := NewGomegaWithT(t)
   455  
   456  		mockCtrl := gomock.NewController(t)
   457  
   458  		scope := mock_aso.NewMockScope(mockCtrl)
   459  		specs := []azure.ASOResourceSpecGetter[*asoresourcesv1.ResourceGroup]{
   460  			mockSpecExpectingResourceRef(mockCtrl, &asoresourcesv1.ResourceGroup{}),
   461  		}
   462  
   463  		deleteErr := errors.New("DeleteResource error")
   464  		postErr := errors.New("PostDeleteHook error")
   465  		reconciler := mock_aso.NewMockReconciler[*asoresourcesv1.ResourceGroup](mockCtrl)
   466  		reconciler.EXPECT().DeleteResource(gomockinternal.AContext(), specs[0].ResourceRef(), serviceName).Return(deleteErr)
   467  		scope.EXPECT().UpdateDeleteStatus(conditionType, serviceName, postErr)
   468  		scope.EXPECT().DefaultedAzureServiceReconcileTimeout().Return(reconcilerutils.DefaultAzureServiceReconcileTimeout)
   469  
   470  		s := &Service[*asoresourcesv1.ResourceGroup, *mock_aso.MockScope]{
   471  			Reconciler:    reconciler,
   472  			Scope:         scope,
   473  			Specs:         specs,
   474  			name:          serviceName,
   475  			ConditionType: conditionType,
   476  			PostDeleteHook: func(_ context.Context, scopeParam *mock_aso.MockScope, err error) error {
   477  				g.Expect(scopeParam).To(BeIdenticalTo(scope))
   478  				g.Expect(err).To(MatchError(deleteErr))
   479  				return postErr
   480  			},
   481  		}
   482  
   483  		err := s.Delete(context.Background())
   484  		g.Expect(err).To(MatchError(postErr))
   485  	})
   486  }
   487  
   488  func TestServicePause(t *testing.T) {
   489  	t.Run("PauseResource succeeds for all resources", func(t *testing.T) {
   490  		g := NewGomegaWithT(t)
   491  
   492  		mockCtrl := gomock.NewController(t)
   493  
   494  		scope := mock_aso.NewMockScope(mockCtrl)
   495  		specs := []azure.ASOResourceSpecGetter[*asoresourcesv1.ResourceGroup]{
   496  			mockSpecExpectingResourceRef(mockCtrl, &asoresourcesv1.ResourceGroup{}),
   497  			mockSpecExpectingResourceRef(mockCtrl, &asoresourcesv1.ResourceGroup{}),
   498  			mockSpecExpectingResourceRef(mockCtrl, &asoresourcesv1.ResourceGroup{}),
   499  		}
   500  
   501  		reconciler := mock_aso.NewMockReconciler[*asoresourcesv1.ResourceGroup](mockCtrl)
   502  		reconciler.EXPECT().PauseResource(gomockinternal.AContext(), specs[0].ResourceRef(), serviceName).Return(nil)
   503  		reconciler.EXPECT().PauseResource(gomockinternal.AContext(), specs[1].ResourceRef(), serviceName).Return(nil)
   504  		reconciler.EXPECT().PauseResource(gomockinternal.AContext(), specs[2].ResourceRef(), serviceName).Return(nil)
   505  
   506  		s := &Service[*asoresourcesv1.ResourceGroup, *mock_aso.MockScope]{
   507  			Reconciler:    reconciler,
   508  			Scope:         scope,
   509  			Specs:         specs,
   510  			name:          serviceName,
   511  			ConditionType: conditionType,
   512  		}
   513  
   514  		err := s.Pause(context.Background())
   515  		g.Expect(err).NotTo(HaveOccurred())
   516  	})
   517  
   518  	t.Run("PauseResource fails for one resource", func(t *testing.T) {
   519  		g := NewGomegaWithT(t)
   520  
   521  		mockCtrl := gomock.NewController(t)
   522  
   523  		scope := mock_aso.NewMockScope(mockCtrl)
   524  		specs := []azure.ASOResourceSpecGetter[*asoresourcesv1.ResourceGroup]{
   525  			mockSpecExpectingResourceRef(mockCtrl, &asoresourcesv1.ResourceGroup{}),
   526  			mockSpecExpectingResourceRef(mockCtrl, &asoresourcesv1.ResourceGroup{
   527  				ObjectMeta: metav1.ObjectMeta{
   528  					Name:      "name",
   529  					Namespace: "namespace",
   530  				},
   531  			}),
   532  			mockSpecExpectingResourceRef(mockCtrl, &asoresourcesv1.ResourceGroup{}),
   533  		}
   534  		scope.EXPECT().ASOOwner().Return(&asoresourcesv1.ResourceGroup{})
   535  
   536  		pauseErr := errors.New("Pause error")
   537  		reconciler := mock_aso.NewMockReconciler[*asoresourcesv1.ResourceGroup](mockCtrl)
   538  		reconciler.EXPECT().PauseResource(gomockinternal.AContext(), specs[0].ResourceRef(), serviceName).Return(nil)
   539  		reconciler.EXPECT().PauseResource(gomockinternal.AContext(), specs[1].ResourceRef(), serviceName).Return(pauseErr)
   540  
   541  		s := &Service[*asoresourcesv1.ResourceGroup, *mock_aso.MockScope]{
   542  			Reconciler:    reconciler,
   543  			Scope:         scope,
   544  			Specs:         specs,
   545  			name:          serviceName,
   546  			ConditionType: conditionType,
   547  		}
   548  
   549  		err := s.Pause(context.Background())
   550  		g.Expect(err).To(MatchError(pauseErr))
   551  	})
   552  }
   553  
   554  func mockSpecExpectingResourceRef(ctrl *gomock.Controller, resource *asoresourcesv1.ResourceGroup) azure.ASOResourceSpecGetter[*asoresourcesv1.ResourceGroup] {
   555  	spec := mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](ctrl)
   556  	spec.EXPECT().ResourceRef().Return(resource).AnyTimes()
   557  	return spec
   558  }