github.com/argoproj/argo-cd/v3@v3.2.1/controller/appcontroller_test.go (about)

     1  package controller
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"testing"
     9  	"time"
    10  
    11  	clustercache "github.com/argoproj/gitops-engine/pkg/cache"
    12  	"github.com/argoproj/gitops-engine/pkg/health"
    13  	"github.com/argoproj/gitops-engine/pkg/utils/kube/kubetest"
    14  	"github.com/sirupsen/logrus"
    15  	"github.com/stretchr/testify/require"
    16  	"k8s.io/apimachinery/pkg/api/resource"
    17  	"k8s.io/apimachinery/pkg/labels"
    18  	"k8s.io/apimachinery/pkg/util/wait"
    19  	"k8s.io/client-go/rest"
    20  	"k8s.io/utils/ptr"
    21  
    22  	"github.com/argoproj/argo-cd/v3/common"
    23  	statecache "github.com/argoproj/argo-cd/v3/controller/cache"
    24  	"github.com/argoproj/argo-cd/v3/controller/sharding"
    25  
    26  	"github.com/argoproj/gitops-engine/pkg/cache/mocks"
    27  	synccommon "github.com/argoproj/gitops-engine/pkg/sync/common"
    28  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
    29  	"github.com/stretchr/testify/assert"
    30  	"github.com/stretchr/testify/mock"
    31  	appsv1 "k8s.io/api/apps/v1"
    32  	corev1 "k8s.io/api/core/v1"
    33  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    36  	"k8s.io/apimachinery/pkg/runtime"
    37  	"k8s.io/apimachinery/pkg/runtime/schema"
    38  	"k8s.io/client-go/kubernetes/fake"
    39  	kubetesting "k8s.io/client-go/testing"
    40  	"k8s.io/client-go/tools/cache"
    41  	"sigs.k8s.io/yaml"
    42  
    43  	dbmocks "github.com/argoproj/argo-cd/v3/util/db/mocks"
    44  
    45  	mockcommitclient "github.com/argoproj/argo-cd/v3/commitserver/apiclient/mocks"
    46  	mockstatecache "github.com/argoproj/argo-cd/v3/controller/cache/mocks"
    47  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    48  	appclientset "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned/fake"
    49  	"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
    50  	mockrepoclient "github.com/argoproj/argo-cd/v3/reposerver/apiclient/mocks"
    51  	"github.com/argoproj/argo-cd/v3/test"
    52  	"github.com/argoproj/argo-cd/v3/util/argo"
    53  	"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
    54  	cacheutil "github.com/argoproj/argo-cd/v3/util/cache"
    55  	appstatecache "github.com/argoproj/argo-cd/v3/util/cache/appstate"
    56  	"github.com/argoproj/argo-cd/v3/util/settings"
    57  	utilTest "github.com/argoproj/argo-cd/v3/util/test"
    58  )
    59  
    60  var testEnableEventList []string = argo.DefaultEnableEventList()
    61  
    62  type namespacedResource struct {
    63  	v1alpha1.ResourceNode
    64  	AppName string
    65  }
    66  
    67  type fakeData struct {
    68  	apps                           []runtime.Object
    69  	manifestResponse               *apiclient.ManifestResponse
    70  	manifestResponses              []*apiclient.ManifestResponse
    71  	managedLiveObjs                map[kube.ResourceKey]*unstructured.Unstructured
    72  	namespacedResources            map[kube.ResourceKey]namespacedResource
    73  	configMapData                  map[string]string
    74  	metricsCacheExpiration         time.Duration
    75  	applicationNamespaces          []string
    76  	updateRevisionForPathsResponse *apiclient.UpdateRevisionForPathsResponse
    77  	additionalObjs                 []runtime.Object
    78  }
    79  
    80  type MockKubectl struct {
    81  	kube.Kubectl
    82  
    83  	DeletedResources []kube.ResourceKey
    84  	CreatedResources []*unstructured.Unstructured
    85  }
    86  
    87  func (m *MockKubectl) CreateResource(ctx context.Context, config *rest.Config, gvk schema.GroupVersionKind, name string, namespace string, obj *unstructured.Unstructured, createOptions metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) {
    88  	m.CreatedResources = append(m.CreatedResources, obj)
    89  	return m.Kubectl.CreateResource(ctx, config, gvk, name, namespace, obj, createOptions, subresources...)
    90  }
    91  
    92  func (m *MockKubectl) DeleteResource(ctx context.Context, config *rest.Config, gvk schema.GroupVersionKind, name string, namespace string, deleteOptions metav1.DeleteOptions) error {
    93  	m.DeletedResources = append(m.DeletedResources, kube.NewResourceKey(gvk.Group, gvk.Kind, namespace, name))
    94  	return m.Kubectl.DeleteResource(ctx, config, gvk, name, namespace, deleteOptions)
    95  }
    96  
    97  func newFakeController(data *fakeData, repoErr error) *ApplicationController {
    98  	return newFakeControllerWithResync(data, time.Minute, repoErr, nil)
    99  }
   100  
   101  func newFakeControllerWithResync(data *fakeData, appResyncPeriod time.Duration, repoErr, revisionPathsErr error) *ApplicationController {
   102  	var clust corev1.Secret
   103  	err := yaml.Unmarshal([]byte(fakeCluster), &clust)
   104  	if err != nil {
   105  		panic(err)
   106  	}
   107  
   108  	// Mock out call to GenerateManifest
   109  	mockRepoClient := mockrepoclient.RepoServerServiceClient{}
   110  
   111  	if len(data.manifestResponses) > 0 {
   112  		for _, response := range data.manifestResponses {
   113  			if repoErr != nil {
   114  				mockRepoClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(response, repoErr).Once()
   115  			} else {
   116  				mockRepoClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(response, nil).Once()
   117  			}
   118  		}
   119  	} else {
   120  		if repoErr != nil {
   121  			mockRepoClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(data.manifestResponse, repoErr).Once()
   122  		} else {
   123  			mockRepoClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(data.manifestResponse, nil).Once()
   124  		}
   125  	}
   126  
   127  	if revisionPathsErr != nil {
   128  		mockRepoClient.On("UpdateRevisionForPaths", mock.Anything, mock.Anything).Return(nil, revisionPathsErr)
   129  	} else {
   130  		mockRepoClient.On("UpdateRevisionForPaths", mock.Anything, mock.Anything).Return(data.updateRevisionForPathsResponse, nil)
   131  	}
   132  
   133  	mockRepoClientset := mockrepoclient.Clientset{RepoServerServiceClient: &mockRepoClient}
   134  
   135  	mockCommitClientset := mockcommitclient.Clientset{}
   136  
   137  	secret := corev1.Secret{
   138  		ObjectMeta: metav1.ObjectMeta{
   139  			Name:      "argocd-secret",
   140  			Namespace: test.FakeArgoCDNamespace,
   141  		},
   142  		Data: map[string][]byte{
   143  			"admin.password":   []byte("test"),
   144  			"server.secretkey": []byte("test"),
   145  		},
   146  	}
   147  	cm := corev1.ConfigMap{
   148  		ObjectMeta: metav1.ObjectMeta{
   149  			Name:      "argocd-cm",
   150  			Namespace: test.FakeArgoCDNamespace,
   151  			Labels: map[string]string{
   152  				"app.kubernetes.io/part-of": "argocd",
   153  			},
   154  		},
   155  		Data: data.configMapData,
   156  	}
   157  	runtimeObjs := []runtime.Object{&clust, &secret, &cm}
   158  	runtimeObjs = append(runtimeObjs, data.additionalObjs...)
   159  	kubeClient := fake.NewClientset(runtimeObjs...)
   160  	settingsMgr := settings.NewSettingsManager(context.Background(), kubeClient, test.FakeArgoCDNamespace)
   161  	kubectl := &MockKubectl{Kubectl: &kubetest.MockKubectlCmd{}}
   162  	ctrl, err := NewApplicationController(
   163  		test.FakeArgoCDNamespace,
   164  		settingsMgr,
   165  		kubeClient,
   166  		appclientset.NewSimpleClientset(data.apps...),
   167  		&mockRepoClientset,
   168  		&mockCommitClientset,
   169  		appstatecache.NewCache(
   170  			cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Minute)),
   171  			1*time.Minute,
   172  		),
   173  		kubectl,
   174  		appResyncPeriod,
   175  		time.Hour,
   176  		time.Second,
   177  		time.Minute,
   178  		nil,
   179  		time.Minute,
   180  		0,
   181  		time.Second*10,
   182  		common.DefaultPortArgoCDMetrics,
   183  		data.metricsCacheExpiration,
   184  		[]string{},
   185  		[]string{},
   186  		[]string{},
   187  		0,
   188  		true,
   189  		nil,
   190  		data.applicationNamespaces,
   191  		nil,
   192  		false,
   193  		false,
   194  		normalizers.IgnoreNormalizerOpts{},
   195  		testEnableEventList,
   196  		false,
   197  	)
   198  	db := &dbmocks.ArgoDB{}
   199  	db.On("GetApplicationControllerReplicas").Return(1)
   200  	// Setting a default sharding algorithm for the tests where we cannot set it.
   201  	ctrl.clusterSharding = sharding.NewClusterSharding(db, 0, 1, common.DefaultShardingAlgorithm)
   202  	if err != nil {
   203  		panic(err)
   204  	}
   205  	cancelProj := test.StartInformer(ctrl.projInformer)
   206  	defer cancelProj()
   207  	cancelApp := test.StartInformer(ctrl.appInformer)
   208  	defer cancelApp()
   209  	clusterCacheMock := mocks.ClusterCache{}
   210  	clusterCacheMock.On("IsNamespaced", mock.Anything).Return(true, nil)
   211  	clusterCacheMock.On("GetOpenAPISchema").Return(nil, nil)
   212  	clusterCacheMock.On("GetGVKParser").Return(nil)
   213  
   214  	mockStateCache := mockstatecache.LiveStateCache{}
   215  	ctrl.appStateManager.(*appStateManager).liveStateCache = &mockStateCache
   216  	ctrl.stateCache = &mockStateCache
   217  	mockStateCache.On("IsNamespaced", mock.Anything, mock.Anything).Return(true, nil)
   218  	mockStateCache.On("GetManagedLiveObjs", mock.Anything, mock.Anything, mock.Anything).Return(data.managedLiveObjs, nil)
   219  	mockStateCache.On("GetVersionsInfo", mock.Anything).Return("v1.2.3", nil, nil)
   220  	response := make(map[kube.ResourceKey]v1alpha1.ResourceNode)
   221  	for k, v := range data.namespacedResources {
   222  		response[k] = v.ResourceNode
   223  	}
   224  	mockStateCache.On("GetNamespaceTopLevelResources", mock.Anything, mock.Anything).Return(response, nil)
   225  	mockStateCache.On("IterateResources", mock.Anything, mock.Anything).Return(nil)
   226  	mockStateCache.On("GetClusterCache", mock.Anything).Return(&clusterCacheMock, nil)
   227  	mockStateCache.On("IterateHierarchyV2", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
   228  		keys := args[1].([]kube.ResourceKey)
   229  		action := args[2].(func(child v1alpha1.ResourceNode, appName string) bool)
   230  		for _, key := range keys {
   231  			appName := ""
   232  			if res, ok := data.namespacedResources[key]; ok {
   233  				appName = res.AppName
   234  			}
   235  			_ = action(v1alpha1.ResourceNode{ResourceRef: v1alpha1.ResourceRef{Kind: key.Kind, Group: key.Group, Namespace: key.Namespace, Name: key.Name}}, appName)
   236  		}
   237  	}).Return(nil)
   238  	return ctrl
   239  }
   240  
   241  var fakeCluster = `
   242  apiVersion: v1
   243  data:
   244    # {"bearerToken":"fake","tlsClientConfig":{"insecure":true},"awsAuthConfig":null}
   245    config: eyJiZWFyZXJUb2tlbiI6ImZha2UiLCJ0bHNDbGllbnRDb25maWciOnsiaW5zZWN1cmUiOnRydWV9LCJhd3NBdXRoQ29uZmlnIjpudWxsfQ==
   246    # minikube
   247    name: bWluaWt1YmU=
   248    # https://localhost:6443
   249    server: aHR0cHM6Ly9sb2NhbGhvc3Q6NjQ0Mw==
   250  kind: Secret
   251  metadata:
   252    labels:
   253      argocd.argoproj.io/secret-type: cluster
   254    name: some-secret
   255    namespace: ` + test.FakeArgoCDNamespace + `
   256  type: Opaque
   257  `
   258  
   259  var fakeApp = `
   260  apiVersion: argoproj.io/v1alpha1
   261  kind: Application
   262  metadata:
   263    uid: "123"
   264    name: my-app
   265    namespace: ` + test.FakeArgoCDNamespace + `
   266  spec:
   267    destination:
   268      namespace: ` + test.FakeDestNamespace + `
   269      server: https://localhost:6443
   270    project: default
   271    source:
   272      path: some/path
   273      repoURL: https://github.com/argoproj/argocd-example-apps.git
   274    syncPolicy:
   275      automated: {}
   276  status:
   277    operationState:
   278      finishedAt: 2018-09-21T23:50:29Z
   279      message: successfully synced
   280      operation:
   281        sync:
   282          revision: HEAD
   283      phase: Succeeded
   284      startedAt: 2018-09-21T23:50:25Z
   285      syncResult:
   286        resources:
   287        - kind: RoleBinding
   288          message: |-
   289            rolebinding.rbac.authorization.k8s.io/always-outofsync reconciled
   290            rolebinding.rbac.authorization.k8s.io/always-outofsync configured
   291          name: always-outofsync
   292          namespace: default
   293          status: Synced
   294        revision: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
   295        source:
   296          path: some/path
   297          repoURL: https://github.com/argoproj/argocd-example-apps.git
   298  `
   299  
   300  var fakeMultiSourceApp = `
   301  apiVersion: argoproj.io/v1alpha1
   302  kind: Application
   303  metadata:
   304    uid: "123"
   305    name: my-app
   306    namespace: ` + test.FakeArgoCDNamespace + `
   307  spec:
   308    destination:
   309      namespace: ` + test.FakeDestNamespace + `
   310      server: https://localhost:6443
   311    project: default
   312    sources:
   313    - path: some/path
   314      helm:
   315        valueFiles:
   316        - $values_test/values.yaml
   317      repoURL: https://github.com/argoproj/argocd-example-apps.git
   318    - path: some/other/path
   319      repoURL: https://github.com/argoproj/argocd-example-apps-fake.git
   320    - ref: values_test
   321      repoURL: https://github.com/argoproj/argocd-example-apps-fake-ref.git
   322    syncPolicy:
   323      automated: {}
   324  status:
   325    operationState:
   326      finishedAt: 2018-09-21T23:50:29Z
   327      message: successfully synced
   328      operation:
   329        sync:
   330          revisions:
   331          - HEAD
   332          - HEAD
   333          - HEAD
   334      phase: Succeeded
   335      startedAt: 2018-09-21T23:50:25Z
   336      syncResult:
   337        resources:
   338        - kind: RoleBinding
   339          message: |-
   340            rolebinding.rbac.authorization.k8s.io/always-outofsync reconciled
   341            rolebinding.rbac.authorization.k8s.io/always-outofsync configured
   342          name: always-outofsync
   343          namespace: default
   344          status: Synced
   345        revisions:
   346        - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
   347        - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
   348        - cccccccccccccccccccccccccccccccccccccccc
   349        sources:
   350        - path: some/path
   351          helm:
   352            valueFiles:
   353            - $values_test/values.yaml
   354          repoURL: https://github.com/argoproj/argocd-example-apps.git
   355        - path: some/other/path
   356          repoURL: https://github.com/argoproj/argocd-example-apps-fake.git
   357        - ref: values_test
   358          repoURL: https://github.com/argoproj/argocd-example-apps-fake-ref.git
   359  `
   360  
   361  var fakeAppWithDestName = `
   362  apiVersion: argoproj.io/v1alpha1
   363  kind: Application
   364  metadata:
   365    uid: "123"
   366    name: my-app
   367    namespace: ` + test.FakeArgoCDNamespace + `
   368  spec:
   369    destination:
   370      namespace: ` + test.FakeDestNamespace + `
   371      name: minikube
   372    project: default
   373    source:
   374      path: some/path
   375      repoURL: https://github.com/argoproj/argocd-example-apps.git
   376    syncPolicy:
   377      automated: {}
   378  `
   379  
   380  var fakeAppWithDestMismatch = `
   381  apiVersion: argoproj.io/v1alpha1
   382  kind: Application
   383  metadata:
   384    uid: "123"
   385    name: my-app
   386    namespace: ` + test.FakeArgoCDNamespace + `
   387  spec:
   388    destination:
   389      namespace: ` + test.FakeDestNamespace + `
   390      name: another-cluster
   391      server: https://localhost:6443
   392    project: default
   393    source:
   394      path: some/path
   395      repoURL: https://github.com/argoproj/argocd-example-apps.git
   396    syncPolicy:
   397      automated: {}
   398  `
   399  
   400  var fakeStrayResource = `
   401  apiVersion: v1
   402  kind: ConfigMap
   403  metadata:
   404    name: test-cm
   405    namespace: invalid
   406    labels:
   407      app.kubernetes.io/instance: my-app
   408  data:
   409  `
   410  
   411  var fakePostDeleteHook = `
   412  {
   413    "apiVersion": "batch/v1",
   414    "kind": "Job",
   415    "metadata": {
   416      "name": "post-delete-hook",
   417      "namespace": "default",
   418      "labels": {
   419        "app.kubernetes.io/instance": "my-app"
   420      },
   421      "annotations": {
   422        "argocd.argoproj.io/hook": "PostDelete",
   423        "argocd.argoproj.io/hook-delete-policy": "HookSucceeded"
   424      }
   425    },
   426    "spec": {
   427      "template": {
   428        "metadata": {
   429          "name": "post-delete-hook"
   430        },
   431        "spec": {
   432          "containers": [
   433            {
   434              "name": "post-delete-hook",
   435              "image": "busybox",
   436              "command": [
   437                "/bin/sh",
   438                "-c",
   439                "sleep 5 && echo hello from the post-delete-hook job"
   440              ]
   441            }
   442          ],
   443          "restartPolicy": "Never"
   444        }
   445      }
   446    }
   447  }
   448  `
   449  
   450  var fakeServiceAccount = `
   451  {
   452    "apiVersion": "v1",
   453    "kind": "ServiceAccount",
   454    "metadata": {
   455      "name": "hook-serviceaccount",
   456      "namespace": "default",
   457      "annotations": {
   458        "argocd.argoproj.io/hook": "PostDelete",
   459        "argocd.argoproj.io/hook-delete-policy": "BeforeHookCreation,HookSucceeded"
   460      }
   461    }
   462  }
   463  `
   464  
   465  var fakeRole = `
   466  {
   467    "apiVersion": "rbac.authorization.k8s.io/v1",
   468    "kind": "Role",
   469    "metadata": {
   470      "name": "hook-role",
   471      "namespace": "default",
   472      "annotations": {
   473        "argocd.argoproj.io/hook": "PostDelete",
   474        "argocd.argoproj.io/hook-delete-policy": "BeforeHookCreation,HookSucceeded"
   475      }
   476    },
   477    "rules": [
   478      {
   479        "apiGroups": [""],
   480        "resources": ["secrets"],
   481        "verbs": ["get", "delete", "list"]
   482      }
   483    ]
   484  }
   485  `
   486  
   487  var fakeRoleBinding = `
   488  {
   489    "apiVersion": "rbac.authorization.k8s.io/v1",
   490    "kind": "RoleBinding",
   491    "metadata": {
   492      "name": "hook-rolebinding",
   493      "namespace": "default",
   494      "annotations": {
   495        "argocd.argoproj.io/hook": "PostDelete",
   496        "argocd.argoproj.io/hook-delete-policy": "BeforeHookCreation,HookSucceeded"
   497      }
   498    },
   499    "roleRef": {
   500      "apiGroup": "rbac.authorization.k8s.io",
   501      "kind": "Role",
   502      "name": "hook-role"
   503    },
   504    "subjects": [
   505      {
   506        "kind": "ServiceAccount",
   507        "name": "hook-serviceaccount",
   508        "namespace": "default"
   509      }
   510    ]
   511  }
   512  `
   513  
   514  func newFakeApp() *v1alpha1.Application {
   515  	return createFakeApp(fakeApp)
   516  }
   517  
   518  func newFakeAppWithHealthAndTime(status health.HealthStatusCode, timestamp metav1.Time) *v1alpha1.Application {
   519  	return createFakeAppWithHealthAndTime(fakeApp, status, timestamp)
   520  }
   521  
   522  func newFakeMultiSourceApp() *v1alpha1.Application {
   523  	return createFakeApp(fakeMultiSourceApp)
   524  }
   525  
   526  func createFakeAppWithHealthAndTime(testApp string, status health.HealthStatusCode, timestamp metav1.Time) *v1alpha1.Application {
   527  	app := createFakeApp(testApp)
   528  	app.Status.Health = v1alpha1.AppHealthStatus{
   529  		Status:             status,
   530  		LastTransitionTime: &timestamp,
   531  	}
   532  	return app
   533  }
   534  
   535  func newFakeAppWithDestMismatch() *v1alpha1.Application {
   536  	return createFakeApp(fakeAppWithDestMismatch)
   537  }
   538  
   539  func newFakeAppWithDestName() *v1alpha1.Application {
   540  	return createFakeApp(fakeAppWithDestName)
   541  }
   542  
   543  func createFakeApp(testApp string) *v1alpha1.Application {
   544  	var app v1alpha1.Application
   545  	err := yaml.Unmarshal([]byte(testApp), &app)
   546  	if err != nil {
   547  		panic(err)
   548  	}
   549  	return &app
   550  }
   551  
   552  func newFakeCM() map[string]any {
   553  	var cm map[string]any
   554  	err := yaml.Unmarshal([]byte(fakeStrayResource), &cm)
   555  	if err != nil {
   556  		panic(err)
   557  	}
   558  	return cm
   559  }
   560  
   561  func newFakePostDeleteHook() map[string]any {
   562  	var hook map[string]any
   563  	err := yaml.Unmarshal([]byte(fakePostDeleteHook), &hook)
   564  	if err != nil {
   565  		panic(err)
   566  	}
   567  	return hook
   568  }
   569  
   570  func newFakeRoleBinding() map[string]any {
   571  	var roleBinding map[string]any
   572  	err := yaml.Unmarshal([]byte(fakeRoleBinding), &roleBinding)
   573  	if err != nil {
   574  		panic(err)
   575  	}
   576  	return roleBinding
   577  }
   578  
   579  func newFakeRole() map[string]any {
   580  	var role map[string]any
   581  	err := yaml.Unmarshal([]byte(fakeRole), &role)
   582  	if err != nil {
   583  		panic(err)
   584  	}
   585  	return role
   586  }
   587  
   588  func newFakeServiceAccount() map[string]any {
   589  	var serviceAccount map[string]any
   590  	err := yaml.Unmarshal([]byte(fakeServiceAccount), &serviceAccount)
   591  	if err != nil {
   592  		panic(err)
   593  	}
   594  	return serviceAccount
   595  }
   596  
   597  func TestAutoSync(t *testing.T) {
   598  	app := newFakeApp()
   599  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
   600  	syncStatus := v1alpha1.SyncStatus{
   601  		Status:   v1alpha1.SyncStatusCodeOutOfSync,
   602  		Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
   603  	}
   604  	cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
   605  	assert.Nil(t, cond)
   606  	app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
   607  	require.NoError(t, err)
   608  	assert.NotNil(t, app.Operation)
   609  	assert.NotNil(t, app.Operation.Sync)
   610  	assert.False(t, app.Operation.Sync.Prune)
   611  }
   612  
   613  func TestAutoSyncEnabledSetToTrue(t *testing.T) {
   614  	app := newFakeApp()
   615  	enable := true
   616  	app.Spec.SyncPolicy.Automated = &v1alpha1.SyncPolicyAutomated{Enabled: &enable}
   617  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
   618  	syncStatus := v1alpha1.SyncStatus{
   619  		Status:   v1alpha1.SyncStatusCodeOutOfSync,
   620  		Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
   621  	}
   622  	cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
   623  	assert.Nil(t, cond)
   624  	app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
   625  	require.NoError(t, err)
   626  	assert.NotNil(t, app.Operation)
   627  	assert.NotNil(t, app.Operation.Sync)
   628  	assert.False(t, app.Operation.Sync.Prune)
   629  }
   630  
   631  func TestAutoSyncMultiSourceWithoutSelfHeal(t *testing.T) {
   632  	// Simulate OutOfSync caused by object change in cluster
   633  	// So our Sync Revisions and SyncStatus Revisions should deep equal
   634  	t.Run("ClusterObjectChangeShouldNotTriggerAutoSync", func(t *testing.T) {
   635  		app := newFakeMultiSourceApp()
   636  		app.Spec.SyncPolicy.Automated.SelfHeal = false
   637  		app.Status.OperationState.SyncResult.Revisions = []string{"z", "x", "v"}
   638  		ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
   639  		syncStatus := v1alpha1.SyncStatus{
   640  			Status:    v1alpha1.SyncStatusCodeOutOfSync,
   641  			Revisions: []string{"z", "x", "v"},
   642  		}
   643  		cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook-1", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
   644  		assert.Nil(t, cond)
   645  		app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
   646  		require.NoError(t, err)
   647  		assert.Nil(t, app.Operation)
   648  	})
   649  	t.Run("NewRevisionChangeShouldTriggerAutoSync", func(t *testing.T) {
   650  		app := newFakeMultiSourceApp()
   651  		app.Spec.SyncPolicy.Automated.SelfHeal = false
   652  		app.Status.OperationState.SyncResult.Revisions = []string{"z", "x", "v"}
   653  		ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
   654  		syncStatus := v1alpha1.SyncStatus{
   655  			Status:    v1alpha1.SyncStatusCodeOutOfSync,
   656  			Revisions: []string{"a", "b", "c"},
   657  		}
   658  		cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook-1", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
   659  		assert.Nil(t, cond)
   660  		app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
   661  		require.NoError(t, err)
   662  		assert.NotNil(t, app.Operation)
   663  	})
   664  }
   665  
   666  func TestAutoSyncNotAllowEmpty(t *testing.T) {
   667  	app := newFakeApp()
   668  	app.Spec.SyncPolicy.Automated.Prune = true
   669  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
   670  	syncStatus := v1alpha1.SyncStatus{
   671  		Status:   v1alpha1.SyncStatusCodeOutOfSync,
   672  		Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
   673  	}
   674  	cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
   675  	assert.NotNil(t, cond)
   676  }
   677  
   678  func TestAutoSyncAllowEmpty(t *testing.T) {
   679  	app := newFakeApp()
   680  	app.Spec.SyncPolicy.Automated.Prune = true
   681  	app.Spec.SyncPolicy.Automated.AllowEmpty = true
   682  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
   683  	syncStatus := v1alpha1.SyncStatus{
   684  		Status:   v1alpha1.SyncStatusCodeOutOfSync,
   685  		Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
   686  	}
   687  	cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
   688  	assert.Nil(t, cond)
   689  }
   690  
   691  func TestSkipAutoSync(t *testing.T) {
   692  	// Verify we skip when we previously synced to it in our most recent history
   693  	// Set current to 'aaaaa', desired to 'aaaa' and mark system OutOfSync
   694  	t.Run("PreviouslySyncedToRevision", func(t *testing.T) {
   695  		app := newFakeApp()
   696  		ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
   697  		syncStatus := v1alpha1.SyncStatus{
   698  			Status:   v1alpha1.SyncStatusCodeOutOfSync,
   699  			Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
   700  		}
   701  		cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
   702  		assert.Nil(t, cond)
   703  		app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
   704  		require.NoError(t, err)
   705  		assert.Nil(t, app.Operation)
   706  	})
   707  
   708  	// Verify we skip when we are already Synced (even if revision is different)
   709  	t.Run("AlreadyInSyncedState", func(t *testing.T) {
   710  		app := newFakeApp()
   711  		ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
   712  		syncStatus := v1alpha1.SyncStatus{
   713  			Status:   v1alpha1.SyncStatusCodeSynced,
   714  			Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
   715  		}
   716  		cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
   717  		assert.Nil(t, cond)
   718  		app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
   719  		require.NoError(t, err)
   720  		assert.Nil(t, app.Operation)
   721  	})
   722  
   723  	// Verify we skip when auto-sync is disabled
   724  	t.Run("AutoSyncIsDisabled", func(t *testing.T) {
   725  		app := newFakeApp()
   726  		app.Spec.SyncPolicy = nil
   727  		ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
   728  		syncStatus := v1alpha1.SyncStatus{
   729  			Status:   v1alpha1.SyncStatusCodeOutOfSync,
   730  			Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
   731  		}
   732  		cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
   733  		assert.Nil(t, cond)
   734  		app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
   735  		require.NoError(t, err)
   736  		assert.Nil(t, app.Operation)
   737  	})
   738  
   739  	// Verify we skip when auto-sync is disabled
   740  	t.Run("AutoSyncEnableFieldIsSetFalse", func(t *testing.T) {
   741  		app := newFakeApp()
   742  		enable := false
   743  		app.Spec.SyncPolicy.Automated = &v1alpha1.SyncPolicyAutomated{Enabled: &enable}
   744  		ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
   745  		syncStatus := v1alpha1.SyncStatus{
   746  			Status:   v1alpha1.SyncStatusCodeOutOfSync,
   747  			Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
   748  		}
   749  		cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
   750  		assert.Nil(t, cond)
   751  		app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
   752  		require.NoError(t, err)
   753  		assert.Nil(t, app.Operation)
   754  	})
   755  
   756  	// Verify we skip when application is marked for deletion
   757  	t.Run("ApplicationIsMarkedForDeletion", func(t *testing.T) {
   758  		app := newFakeApp()
   759  		now := metav1.Now()
   760  		app.DeletionTimestamp = &now
   761  		ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
   762  		syncStatus := v1alpha1.SyncStatus{
   763  			Status:   v1alpha1.SyncStatusCodeOutOfSync,
   764  			Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
   765  		}
   766  		cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
   767  		assert.Nil(t, cond)
   768  		app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
   769  		require.NoError(t, err)
   770  		assert.Nil(t, app.Operation)
   771  	})
   772  
   773  	// Verify we skip when previous sync attempt failed and return error condition
   774  	// Set current to 'aaaaa', desired to 'bbbbb' and add 'bbbbb' to failure history
   775  	t.Run("PreviousSyncAttemptFailed", func(t *testing.T) {
   776  		app := newFakeApp()
   777  		app.Status.OperationState = &v1alpha1.OperationState{
   778  			Operation: v1alpha1.Operation{
   779  				Sync: &v1alpha1.SyncOperation{},
   780  			},
   781  			Phase: synccommon.OperationFailed,
   782  			SyncResult: &v1alpha1.SyncOperationResult{
   783  				Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
   784  				Source:   *app.Spec.Source.DeepCopy(),
   785  			},
   786  		}
   787  		ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
   788  		syncStatus := v1alpha1.SyncStatus{
   789  			Status:   v1alpha1.SyncStatusCodeOutOfSync,
   790  			Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
   791  		}
   792  		cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
   793  		assert.NotNil(t, cond)
   794  		app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
   795  		require.NoError(t, err)
   796  		assert.Nil(t, app.Operation)
   797  	})
   798  
   799  	t.Run("PreviousSyncAttemptError", func(t *testing.T) {
   800  		app := newFakeApp()
   801  		app.Status.OperationState = &v1alpha1.OperationState{
   802  			Operation: v1alpha1.Operation{
   803  				Sync: &v1alpha1.SyncOperation{},
   804  			},
   805  			Phase: synccommon.OperationError,
   806  			SyncResult: &v1alpha1.SyncOperationResult{
   807  				Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
   808  				Source:   *app.Spec.Source.DeepCopy(),
   809  			},
   810  		}
   811  		ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
   812  		syncStatus := v1alpha1.SyncStatus{
   813  			Status:   v1alpha1.SyncStatusCodeOutOfSync,
   814  			Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
   815  		}
   816  		cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
   817  		assert.NotNil(t, cond)
   818  		app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
   819  		require.NoError(t, err)
   820  		assert.Nil(t, app.Operation)
   821  	})
   822  
   823  	t.Run("NeedsToPruneResourcesOnlyButAutomatedPruneDisabled", func(t *testing.T) {
   824  		app := newFakeApp()
   825  		ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
   826  		syncStatus := v1alpha1.SyncStatus{
   827  			Status:   v1alpha1.SyncStatusCodeOutOfSync,
   828  			Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
   829  		}
   830  		cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{
   831  			{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync, RequiresPruning: true},
   832  		}, true)
   833  		assert.Nil(t, cond)
   834  		app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
   835  		require.NoError(t, err)
   836  		assert.Nil(t, app.Operation)
   837  	})
   838  }
   839  
   840  // TestAutoSyncIndicateError verifies we skip auto-sync and return error condition if previous sync failed
   841  func TestAutoSyncIndicateError(t *testing.T) {
   842  	app := newFakeApp()
   843  	app.Spec.Source.Helm = &v1alpha1.ApplicationSourceHelm{
   844  		Parameters: []v1alpha1.HelmParameter{
   845  			{
   846  				Name:  "a",
   847  				Value: "1",
   848  			},
   849  		},
   850  	}
   851  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
   852  	syncStatus := v1alpha1.SyncStatus{
   853  		Status:   v1alpha1.SyncStatusCodeOutOfSync,
   854  		Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
   855  	}
   856  	app.Status.OperationState = &v1alpha1.OperationState{
   857  		Operation: v1alpha1.Operation{
   858  			Sync: &v1alpha1.SyncOperation{
   859  				Source: app.Spec.Source.DeepCopy(),
   860  			},
   861  		},
   862  		Phase: synccommon.OperationFailed,
   863  		SyncResult: &v1alpha1.SyncOperationResult{
   864  			Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
   865  			Source:   *app.Spec.Source.DeepCopy(),
   866  		},
   867  	}
   868  	cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
   869  	assert.NotNil(t, cond)
   870  	app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
   871  	require.NoError(t, err)
   872  	assert.Nil(t, app.Operation)
   873  }
   874  
   875  // TestAutoSyncParameterOverrides verifies we auto-sync if revision is same but parameter overrides are different
   876  func TestAutoSyncParameterOverrides(t *testing.T) {
   877  	t.Run("Single source", func(t *testing.T) {
   878  		app := newFakeApp()
   879  		app.Spec.Source.Helm = &v1alpha1.ApplicationSourceHelm{
   880  			Parameters: []v1alpha1.HelmParameter{
   881  				{
   882  					Name:  "a",
   883  					Value: "1",
   884  				},
   885  			},
   886  		}
   887  		app.Status.OperationState = &v1alpha1.OperationState{
   888  			Operation: v1alpha1.Operation{
   889  				Sync: &v1alpha1.SyncOperation{
   890  					Source: &v1alpha1.ApplicationSource{
   891  						Helm: &v1alpha1.ApplicationSourceHelm{
   892  							Parameters: []v1alpha1.HelmParameter{
   893  								{
   894  									Name:  "a",
   895  									Value: "2", // this value changed
   896  								},
   897  							},
   898  						},
   899  					},
   900  				},
   901  			},
   902  			Phase: synccommon.OperationFailed,
   903  			SyncResult: &v1alpha1.SyncOperationResult{
   904  				Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
   905  			},
   906  		}
   907  		syncStatus := v1alpha1.SyncStatus{
   908  			Status:   v1alpha1.SyncStatusCodeOutOfSync,
   909  			Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
   910  		}
   911  		ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
   912  		cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
   913  		assert.Nil(t, cond)
   914  		app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
   915  		require.NoError(t, err)
   916  		assert.NotNil(t, app.Operation)
   917  	})
   918  
   919  	t.Run("Multi sources", func(t *testing.T) {
   920  		app := newFakeMultiSourceApp()
   921  		app.Spec.Sources[0].Helm = &v1alpha1.ApplicationSourceHelm{
   922  			Parameters: []v1alpha1.HelmParameter{
   923  				{
   924  					Name:  "a",
   925  					Value: "1",
   926  				},
   927  			},
   928  		}
   929  		ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
   930  		app.Status.OperationState.SyncResult.Revisions = []string{"z", "x", "v"}
   931  		app.Status.OperationState.SyncResult.Sources[0].Helm = &v1alpha1.ApplicationSourceHelm{
   932  			Parameters: []v1alpha1.HelmParameter{
   933  				{
   934  					Name:  "a",
   935  					Value: "2", // this value changed
   936  				},
   937  			},
   938  		}
   939  		syncStatus := v1alpha1.SyncStatus{
   940  			Status:    v1alpha1.SyncStatusCodeOutOfSync,
   941  			Revisions: []string{"z", "x", "v"},
   942  		}
   943  		cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
   944  		assert.Nil(t, cond)
   945  		app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
   946  		require.NoError(t, err)
   947  		assert.NotNil(t, app.Operation)
   948  	})
   949  }
   950  
   951  // TestFinalizeAppDeletion verifies application deletion
   952  func TestFinalizeAppDeletion(t *testing.T) {
   953  	now := metav1.Now()
   954  	defaultProj := v1alpha1.AppProject{
   955  		ObjectMeta: metav1.ObjectMeta{
   956  			Name:      "default",
   957  			Namespace: test.FakeArgoCDNamespace,
   958  		},
   959  		Spec: v1alpha1.AppProjectSpec{
   960  			SourceRepos: []string{"*"},
   961  			Destinations: []v1alpha1.ApplicationDestination{
   962  				{
   963  					Server:    "*",
   964  					Namespace: "*",
   965  				},
   966  			},
   967  		},
   968  	}
   969  
   970  	// Ensure app can be deleted cascading
   971  	t.Run("CascadingDelete", func(t *testing.T) {
   972  		app := newFakeApp()
   973  		app.SetCascadedDeletion(v1alpha1.ResourcesFinalizerName)
   974  		app.DeletionTimestamp = &now
   975  		app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
   976  		ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}, managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{}}, nil)
   977  		patched := false
   978  		fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
   979  		defaultReactor := fakeAppCs.ReactionChain[0]
   980  		fakeAppCs.ReactionChain = nil
   981  		fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
   982  			return defaultReactor.React(action)
   983  		})
   984  		fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) {
   985  			patched = true
   986  			return true, &v1alpha1.Application{}, nil
   987  		})
   988  		err := ctrl.finalizeApplicationDeletion(app, func(_ string) ([]*v1alpha1.Cluster, error) {
   989  			return []*v1alpha1.Cluster{}, nil
   990  		})
   991  		require.NoError(t, err)
   992  		assert.True(t, patched)
   993  	})
   994  
   995  	// Ensure any stray resources irregularly labeled with instance label of app are not deleted upon deleting,
   996  	// when app project restriction is in place
   997  	t.Run("ProjectRestrictionEnforced", func(t *testing.T) {
   998  		restrictedProj := v1alpha1.AppProject{
   999  			ObjectMeta: metav1.ObjectMeta{
  1000  				Name:      "restricted",
  1001  				Namespace: test.FakeArgoCDNamespace,
  1002  			},
  1003  			Spec: v1alpha1.AppProjectSpec{
  1004  				SourceRepos: []string{"*"},
  1005  				Destinations: []v1alpha1.ApplicationDestination{
  1006  					{
  1007  						Server:    "*",
  1008  						Namespace: "my-app",
  1009  					},
  1010  				},
  1011  			},
  1012  		}
  1013  		app := newFakeApp()
  1014  		app.SetCascadedDeletion(v1alpha1.ResourcesFinalizerName)
  1015  		app.DeletionTimestamp = &now
  1016  		app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
  1017  		app.Spec.Project = "restricted"
  1018  		appObj := kube.MustToUnstructured(&app)
  1019  		cm := newFakeCM()
  1020  		strayObj := kube.MustToUnstructured(&cm)
  1021  		ctrl := newFakeController(&fakeData{
  1022  			apps: []runtime.Object{app, &defaultProj, &restrictedProj},
  1023  			managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
  1024  				kube.GetResourceKey(appObj):   appObj,
  1025  				kube.GetResourceKey(strayObj): strayObj,
  1026  			},
  1027  		}, nil)
  1028  
  1029  		patched := false
  1030  		fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  1031  		defaultReactor := fakeAppCs.ReactionChain[0]
  1032  		fakeAppCs.ReactionChain = nil
  1033  		fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  1034  			return defaultReactor.React(action)
  1035  		})
  1036  		fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  1037  			patched = true
  1038  			return true, &v1alpha1.Application{}, nil
  1039  		})
  1040  		err := ctrl.finalizeApplicationDeletion(app, func(_ string) ([]*v1alpha1.Cluster, error) {
  1041  			return []*v1alpha1.Cluster{}, nil
  1042  		})
  1043  		require.NoError(t, err)
  1044  		assert.True(t, patched)
  1045  		objsMap, err := ctrl.stateCache.GetManagedLiveObjs(&v1alpha1.Cluster{Server: "test", Name: "test"}, app, []*unstructured.Unstructured{})
  1046  		if err != nil {
  1047  			require.NoError(t, err)
  1048  		}
  1049  		// Managed objects must be empty
  1050  		assert.Empty(t, objsMap)
  1051  
  1052  		// Loop through all deleted objects, ensure that test-cm is none of them
  1053  		for _, o := range ctrl.kubectl.(*MockKubectl).DeletedResources {
  1054  			assert.NotEqual(t, "test-cm", o.Name)
  1055  		}
  1056  	})
  1057  
  1058  	t.Run("DeleteWithDestinationClusterName", func(t *testing.T) {
  1059  		app := newFakeAppWithDestName()
  1060  		app.SetCascadedDeletion(v1alpha1.ResourcesFinalizerName)
  1061  		app.DeletionTimestamp = &now
  1062  		ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}, managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{}}, nil)
  1063  		patched := false
  1064  		fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  1065  		defaultReactor := fakeAppCs.ReactionChain[0]
  1066  		fakeAppCs.ReactionChain = nil
  1067  		fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  1068  			return defaultReactor.React(action)
  1069  		})
  1070  		fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  1071  			patched = true
  1072  			return true, &v1alpha1.Application{}, nil
  1073  		})
  1074  		err := ctrl.finalizeApplicationDeletion(app, func(_ string) ([]*v1alpha1.Cluster, error) {
  1075  			return []*v1alpha1.Cluster{}, nil
  1076  		})
  1077  		require.NoError(t, err)
  1078  		assert.True(t, patched)
  1079  	})
  1080  
  1081  	// Create an Application with a cluster that doesn't exist
  1082  	// Ensure it can be deleted.
  1083  	t.Run("DeleteWithInvalidClusterName", func(t *testing.T) {
  1084  		appTemplate := newFakeAppWithDestName()
  1085  
  1086  		testShouldDelete := func(app *v1alpha1.Application) {
  1087  			appObj := kube.MustToUnstructured(&app)
  1088  			ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}, managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
  1089  				kube.GetResourceKey(appObj): appObj,
  1090  			}}, nil)
  1091  
  1092  			fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  1093  			defaultReactor := fakeAppCs.ReactionChain[0]
  1094  			fakeAppCs.ReactionChain = nil
  1095  			fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  1096  				return defaultReactor.React(action)
  1097  			})
  1098  			err := ctrl.finalizeApplicationDeletion(app, func(_ string) ([]*v1alpha1.Cluster, error) {
  1099  				return []*v1alpha1.Cluster{}, nil
  1100  			})
  1101  			require.NoError(t, err)
  1102  		}
  1103  
  1104  		app1 := appTemplate.DeepCopy()
  1105  		app1.Spec.Destination.Server = "https://invalid"
  1106  		testShouldDelete(app1)
  1107  
  1108  		app2 := appTemplate.DeepCopy()
  1109  		app2.Spec.Destination.Name = "invalid"
  1110  		testShouldDelete(app2)
  1111  
  1112  		app3 := appTemplate.DeepCopy()
  1113  		app3.Spec.Destination.Name = "invalid"
  1114  		app3.Spec.Destination.Server = "https://invalid"
  1115  		testShouldDelete(app3)
  1116  	})
  1117  
  1118  	t.Run("PostDelete_HookIsCreated", func(t *testing.T) {
  1119  		app := newFakeApp()
  1120  		app.SetPostDeleteFinalizer()
  1121  		app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
  1122  		ctrl := newFakeController(&fakeData{
  1123  			manifestResponses: []*apiclient.ManifestResponse{{
  1124  				Manifests: []string{fakePostDeleteHook},
  1125  			}},
  1126  			apps:            []runtime.Object{app, &defaultProj},
  1127  			managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{},
  1128  		}, nil)
  1129  
  1130  		patched := false
  1131  		fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  1132  		defaultReactor := fakeAppCs.ReactionChain[0]
  1133  		fakeAppCs.ReactionChain = nil
  1134  		fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  1135  			return defaultReactor.React(action)
  1136  		})
  1137  		fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  1138  			patched = true
  1139  			return true, &v1alpha1.Application{}, nil
  1140  		})
  1141  		err := ctrl.finalizeApplicationDeletion(app, func(_ string) ([]*v1alpha1.Cluster, error) {
  1142  			return []*v1alpha1.Cluster{}, nil
  1143  		})
  1144  		require.NoError(t, err)
  1145  		// finalizer is not deleted
  1146  		assert.False(t, patched)
  1147  		// post-delete hook is created
  1148  		require.Len(t, ctrl.kubectl.(*MockKubectl).CreatedResources, 1)
  1149  		require.Equal(t, "post-delete-hook", ctrl.kubectl.(*MockKubectl).CreatedResources[0].GetName())
  1150  	})
  1151  
  1152  	t.Run("PostDelete_HookIsExecuted", func(t *testing.T) {
  1153  		app := newFakeApp()
  1154  		app.SetPostDeleteFinalizer()
  1155  		app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
  1156  		liveHook := &unstructured.Unstructured{Object: newFakePostDeleteHook()}
  1157  		conditions := []any{
  1158  			map[string]any{
  1159  				"type":   "Complete",
  1160  				"status": "True",
  1161  			},
  1162  		}
  1163  		require.NoError(t, unstructured.SetNestedField(liveHook.Object, conditions, "status", "conditions"))
  1164  		ctrl := newFakeController(&fakeData{
  1165  			manifestResponses: []*apiclient.ManifestResponse{{
  1166  				Manifests: []string{fakePostDeleteHook},
  1167  			}},
  1168  			apps: []runtime.Object{app, &defaultProj},
  1169  			managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
  1170  				kube.GetResourceKey(liveHook): liveHook,
  1171  			},
  1172  		}, nil)
  1173  
  1174  		patched := false
  1175  		fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  1176  		defaultReactor := fakeAppCs.ReactionChain[0]
  1177  		fakeAppCs.ReactionChain = nil
  1178  		fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  1179  			return defaultReactor.React(action)
  1180  		})
  1181  		fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  1182  			patched = true
  1183  			return true, &v1alpha1.Application{}, nil
  1184  		})
  1185  		err := ctrl.finalizeApplicationDeletion(app, func(_ string) ([]*v1alpha1.Cluster, error) {
  1186  			return []*v1alpha1.Cluster{}, nil
  1187  		})
  1188  		require.NoError(t, err)
  1189  		// finalizer is removed
  1190  		assert.True(t, patched)
  1191  	})
  1192  
  1193  	t.Run("PostDelete_HookIsDeleted", func(t *testing.T) {
  1194  		app := newFakeApp()
  1195  		app.SetPostDeleteFinalizer("cleanup")
  1196  		app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
  1197  		liveRoleBinding := &unstructured.Unstructured{Object: newFakeRoleBinding()}
  1198  		liveRole := &unstructured.Unstructured{Object: newFakeRole()}
  1199  		liveServiceAccount := &unstructured.Unstructured{Object: newFakeServiceAccount()}
  1200  		liveHook := &unstructured.Unstructured{Object: newFakePostDeleteHook()}
  1201  		conditions := []any{
  1202  			map[string]any{
  1203  				"type":   "Complete",
  1204  				"status": "True",
  1205  			},
  1206  		}
  1207  		require.NoError(t, unstructured.SetNestedField(liveHook.Object, conditions, "status", "conditions"))
  1208  		ctrl := newFakeController(&fakeData{
  1209  			manifestResponses: []*apiclient.ManifestResponse{{
  1210  				Manifests: []string{fakeRoleBinding, fakeRole, fakeServiceAccount, fakePostDeleteHook},
  1211  			}},
  1212  			apps: []runtime.Object{app, &defaultProj},
  1213  			managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
  1214  				kube.GetResourceKey(liveRoleBinding):    liveRoleBinding,
  1215  				kube.GetResourceKey(liveRole):           liveRole,
  1216  				kube.GetResourceKey(liveServiceAccount): liveServiceAccount,
  1217  				kube.GetResourceKey(liveHook):           liveHook,
  1218  			},
  1219  		}, nil)
  1220  
  1221  		patched := false
  1222  		fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  1223  		defaultReactor := fakeAppCs.ReactionChain[0]
  1224  		fakeAppCs.ReactionChain = nil
  1225  		fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  1226  			return defaultReactor.React(action)
  1227  		})
  1228  		fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  1229  			patched = true
  1230  			return true, &v1alpha1.Application{}, nil
  1231  		})
  1232  		err := ctrl.finalizeApplicationDeletion(app, func(_ string) ([]*v1alpha1.Cluster, error) {
  1233  			return []*v1alpha1.Cluster{}, nil
  1234  		})
  1235  		require.NoError(t, err)
  1236  		// post-delete hooks are deleted
  1237  		require.Len(t, ctrl.kubectl.(*MockKubectl).DeletedResources, 4)
  1238  		deletedResources := []string{}
  1239  		for _, res := range ctrl.kubectl.(*MockKubectl).DeletedResources {
  1240  			deletedResources = append(deletedResources, res.Name)
  1241  		}
  1242  		expectedNames := []string{"hook-rolebinding", "hook-role", "hook-serviceaccount", "post-delete-hook"}
  1243  		require.ElementsMatch(t, expectedNames, deletedResources, "Deleted resources should match expected names")
  1244  		// finalizer is not removed
  1245  		assert.False(t, patched)
  1246  	})
  1247  }
  1248  
  1249  // TestNormalizeApplication verifies we normalize an application during reconciliation
  1250  func TestNormalizeApplication(t *testing.T) {
  1251  	defaultProj := v1alpha1.AppProject{
  1252  		ObjectMeta: metav1.ObjectMeta{
  1253  			Name:      "default",
  1254  			Namespace: test.FakeArgoCDNamespace,
  1255  		},
  1256  		Spec: v1alpha1.AppProjectSpec{
  1257  			SourceRepos: []string{"*"},
  1258  			Destinations: []v1alpha1.ApplicationDestination{
  1259  				{
  1260  					Server:    "*",
  1261  					Namespace: "*",
  1262  				},
  1263  			},
  1264  		},
  1265  	}
  1266  	app := newFakeApp()
  1267  	app.Spec.Project = ""
  1268  	app.Spec.Source.Kustomize = &v1alpha1.ApplicationSourceKustomize{NamePrefix: "foo-"}
  1269  	data := fakeData{
  1270  		apps: []runtime.Object{app, &defaultProj},
  1271  		manifestResponse: &apiclient.ManifestResponse{
  1272  			Manifests: []string{},
  1273  			Namespace: test.FakeDestNamespace,
  1274  			Server:    test.FakeClusterURL,
  1275  			Revision:  "abc123",
  1276  		},
  1277  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
  1278  	}
  1279  
  1280  	{
  1281  		// Verify we normalize the app because project is missing
  1282  		ctrl := newFakeController(&data, nil)
  1283  		key, _ := cache.MetaNamespaceKeyFunc(app)
  1284  		ctrl.appRefreshQueue.AddRateLimited(key)
  1285  		fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  1286  		fakeAppCs.ReactionChain = nil
  1287  		normalized := false
  1288  		fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  1289  			if patchAction, ok := action.(kubetesting.PatchAction); ok {
  1290  				if string(patchAction.GetPatch()) == `{"spec":{"project":"default"}}` {
  1291  					normalized = true
  1292  				}
  1293  			}
  1294  			return true, &v1alpha1.Application{}, nil
  1295  		})
  1296  		ctrl.processAppRefreshQueueItem()
  1297  		assert.True(t, normalized)
  1298  	}
  1299  
  1300  	{
  1301  		// Verify we don't unnecessarily normalize app when project is set
  1302  		app.Spec.Project = "default"
  1303  		data.apps[0] = app
  1304  		ctrl := newFakeController(&data, nil)
  1305  		key, _ := cache.MetaNamespaceKeyFunc(app)
  1306  		ctrl.appRefreshQueue.AddRateLimited(key)
  1307  		fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  1308  		fakeAppCs.ReactionChain = nil
  1309  		normalized := false
  1310  		fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  1311  			if patchAction, ok := action.(kubetesting.PatchAction); ok {
  1312  				if string(patchAction.GetPatch()) == `{"spec":{"project":"default"},"status":{"sync":{"comparedTo":{"destination":{},"source":{"repoURL":""}}}}}` {
  1313  					normalized = true
  1314  				}
  1315  			}
  1316  			return true, &v1alpha1.Application{}, nil
  1317  		})
  1318  		ctrl.processAppRefreshQueueItem()
  1319  		assert.False(t, normalized)
  1320  	}
  1321  }
  1322  
  1323  func TestHandleAppUpdated(t *testing.T) {
  1324  	app := newFakeApp()
  1325  	app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
  1326  	app.Spec.Destination.Server = v1alpha1.KubernetesInternalAPIServerAddr
  1327  	proj := defaultProj.DeepCopy()
  1328  	proj.Spec.SourceNamespaces = []string{test.FakeArgoCDNamespace}
  1329  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, proj}}, nil)
  1330  
  1331  	ctrl.handleObjectUpdated(map[string]bool{app.InstanceName(ctrl.namespace): true}, kube.GetObjectRef(kube.MustToUnstructured(app)))
  1332  	isRequested, level := ctrl.isRefreshRequested(app.QualifiedName())
  1333  	assert.False(t, isRequested)
  1334  	assert.Equal(t, ComparisonWithNothing, level)
  1335  
  1336  	ctrl.handleObjectUpdated(map[string]bool{app.InstanceName(ctrl.namespace): true}, corev1.ObjectReference{UID: "test", Kind: kube.DeploymentKind, Name: "test", Namespace: "default"})
  1337  	isRequested, level = ctrl.isRefreshRequested(app.QualifiedName())
  1338  	assert.True(t, isRequested)
  1339  	assert.Equal(t, CompareWithRecent, level)
  1340  }
  1341  
  1342  func TestHandleOrphanedResourceUpdated(t *testing.T) {
  1343  	app1 := newFakeApp()
  1344  	app1.Name = "app1"
  1345  	app1.Spec.Destination.Namespace = test.FakeArgoCDNamespace
  1346  	app1.Spec.Destination.Server = v1alpha1.KubernetesInternalAPIServerAddr
  1347  
  1348  	app2 := newFakeApp()
  1349  	app2.Name = "app2"
  1350  	app2.Spec.Destination.Namespace = test.FakeArgoCDNamespace
  1351  	app2.Spec.Destination.Server = v1alpha1.KubernetesInternalAPIServerAddr
  1352  
  1353  	proj := defaultProj.DeepCopy()
  1354  	proj.Spec.OrphanedResources = &v1alpha1.OrphanedResourcesMonitorSettings{}
  1355  
  1356  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{app1, app2, proj}}, nil)
  1357  
  1358  	ctrl.handleObjectUpdated(map[string]bool{}, corev1.ObjectReference{UID: "test", Kind: kube.DeploymentKind, Name: "test", Namespace: test.FakeArgoCDNamespace})
  1359  
  1360  	isRequested, level := ctrl.isRefreshRequested(app1.QualifiedName())
  1361  	assert.True(t, isRequested)
  1362  	assert.Equal(t, CompareWithRecent, level)
  1363  
  1364  	isRequested, level = ctrl.isRefreshRequested(app2.QualifiedName())
  1365  	assert.True(t, isRequested)
  1366  	assert.Equal(t, CompareWithRecent, level)
  1367  }
  1368  
  1369  func TestGetResourceTree_HasOrphanedResources(t *testing.T) {
  1370  	app := newFakeApp()
  1371  	proj := defaultProj.DeepCopy()
  1372  	proj.Spec.OrphanedResources = &v1alpha1.OrphanedResourcesMonitorSettings{}
  1373  
  1374  	managedDeploy := v1alpha1.ResourceNode{
  1375  		ResourceRef: v1alpha1.ResourceRef{Group: "apps", Kind: "Deployment", Namespace: "default", Name: "nginx-deployment", Version: "v1"},
  1376  		Health: &v1alpha1.HealthStatus{
  1377  			Status: health.HealthStatusMissing,
  1378  		},
  1379  	}
  1380  	orphanedDeploy1 := v1alpha1.ResourceNode{
  1381  		ResourceRef: v1alpha1.ResourceRef{Group: "apps", Kind: "Deployment", Namespace: "default", Name: "deploy1"},
  1382  	}
  1383  	orphanedDeploy2 := v1alpha1.ResourceNode{
  1384  		ResourceRef: v1alpha1.ResourceRef{Group: "apps", Kind: "Deployment", Namespace: "default", Name: "deploy2"},
  1385  	}
  1386  
  1387  	ctrl := newFakeController(&fakeData{
  1388  		apps: []runtime.Object{app, proj},
  1389  		namespacedResources: map[kube.ResourceKey]namespacedResource{
  1390  			kube.NewResourceKey("apps", "Deployment", "default", "nginx-deployment"): {ResourceNode: managedDeploy},
  1391  			kube.NewResourceKey("apps", "Deployment", "default", "deploy1"):          {ResourceNode: orphanedDeploy1},
  1392  			kube.NewResourceKey("apps", "Deployment", "default", "deploy2"):          {ResourceNode: orphanedDeploy2},
  1393  		},
  1394  	}, nil)
  1395  	tree, err := ctrl.getResourceTree(&v1alpha1.Cluster{Server: "https://localhost:6443", Name: "fake-cluster"}, app, []*v1alpha1.ResourceDiff{{
  1396  		Namespace:   "default",
  1397  		Name:        "nginx-deployment",
  1398  		Kind:        "Deployment",
  1399  		Group:       "apps",
  1400  		LiveState:   "null",
  1401  		TargetState: test.DeploymentManifest,
  1402  	}})
  1403  
  1404  	require.NoError(t, err)
  1405  	assert.Equal(t, []v1alpha1.ResourceNode{managedDeploy}, tree.Nodes)
  1406  	assert.Equal(t, []v1alpha1.ResourceNode{orphanedDeploy1, orphanedDeploy2}, tree.OrphanedNodes)
  1407  }
  1408  
  1409  func TestSetOperationStateOnDeletedApp(t *testing.T) {
  1410  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil)
  1411  	fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  1412  	fakeAppCs.ReactionChain = nil
  1413  	patched := false
  1414  	fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  1415  		patched = true
  1416  		return true, &v1alpha1.Application{}, apierrors.NewNotFound(schema.GroupResource{}, "my-app")
  1417  	})
  1418  	ctrl.setOperationState(newFakeApp(), &v1alpha1.OperationState{Phase: synccommon.OperationSucceeded})
  1419  	assert.True(t, patched)
  1420  }
  1421  
  1422  func TestSetOperationStateLogRetries(t *testing.T) {
  1423  	hook := utilTest.LogHook{}
  1424  	logrus.AddHook(&hook)
  1425  	t.Cleanup(func() {
  1426  		logrus.StandardLogger().ReplaceHooks(logrus.LevelHooks{})
  1427  	})
  1428  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil)
  1429  	fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  1430  	fakeAppCs.ReactionChain = nil
  1431  	patched := false
  1432  	fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  1433  		if !patched {
  1434  			patched = true
  1435  			return true, &v1alpha1.Application{}, errors.New("fake error")
  1436  		}
  1437  		return true, &v1alpha1.Application{}, nil
  1438  	})
  1439  	ctrl.setOperationState(newFakeApp(), &v1alpha1.OperationState{Phase: synccommon.OperationSucceeded})
  1440  	assert.True(t, patched)
  1441  	assert.Contains(t, hook.Entries[0].Message, "fake error")
  1442  }
  1443  
  1444  func TestNeedRefreshAppStatus(t *testing.T) {
  1445  	testCases := []struct {
  1446  		name string
  1447  		app  *v1alpha1.Application
  1448  	}{
  1449  		{
  1450  			name: "single-source app",
  1451  			app:  newFakeApp(),
  1452  		},
  1453  		{
  1454  			name: "multi-source app",
  1455  			app:  newFakeMultiSourceApp(),
  1456  		},
  1457  	}
  1458  
  1459  	for _, tc := range testCases {
  1460  		t.Run(tc.name, func(t *testing.T) {
  1461  			app := tc.app
  1462  			now := metav1.Now()
  1463  			app.Status.ReconciledAt = &now
  1464  
  1465  			app.Status.Sync = v1alpha1.SyncStatus{
  1466  				Status: v1alpha1.SyncStatusCodeSynced,
  1467  				ComparedTo: v1alpha1.ComparedTo{
  1468  					Destination:       app.Spec.Destination,
  1469  					IgnoreDifferences: app.Spec.IgnoreDifferences,
  1470  				},
  1471  			}
  1472  
  1473  			if app.Spec.HasMultipleSources() {
  1474  				app.Status.Sync.ComparedTo.Sources = app.Spec.Sources
  1475  			} else {
  1476  				app.Status.Sync.ComparedTo.Source = app.Spec.GetSource()
  1477  			}
  1478  
  1479  			ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil)
  1480  
  1481  			t.Run("no need to refresh just reconciled application", func(t *testing.T) {
  1482  				needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour)
  1483  				assert.False(t, needRefresh)
  1484  			})
  1485  
  1486  			t.Run("requested refresh is respected", func(t *testing.T) {
  1487  				needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour)
  1488  				assert.False(t, needRefresh)
  1489  
  1490  				// use a one-off controller so other tests don't have a manual refresh request
  1491  				ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil)
  1492  
  1493  				// refresh app using the 'deepest' requested comparison level
  1494  				ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), nil)
  1495  				ctrl.requestAppRefresh(app.Name, ComparisonWithNothing.Pointer(), nil)
  1496  
  1497  				needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour)
  1498  				assert.True(t, needRefresh)
  1499  				assert.Equal(t, v1alpha1.RefreshTypeNormal, refreshType)
  1500  				assert.Equal(t, CompareWithRecent, compareWith)
  1501  			})
  1502  
  1503  			t.Run("requesting refresh with delay gives correct compression level", func(t *testing.T) {
  1504  				needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour)
  1505  				assert.False(t, needRefresh)
  1506  
  1507  				// use a one-off controller so other tests don't have a manual refresh request
  1508  				ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil)
  1509  
  1510  				// refresh app with a non-nil delay
  1511  				// use zero-second delay to test the add later logic without waiting in the test
  1512  				delay := time.Duration(0)
  1513  				ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), &delay)
  1514  
  1515  				ctrl.processAppComparisonTypeQueueItem()
  1516  				needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour)
  1517  				assert.True(t, needRefresh)
  1518  				assert.Equal(t, v1alpha1.RefreshTypeNormal, refreshType)
  1519  				assert.Equal(t, CompareWithRecent, compareWith)
  1520  			})
  1521  
  1522  			t.Run("refresh application which status is not reconciled using latest commit", func(t *testing.T) {
  1523  				app := app.DeepCopy()
  1524  				needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour)
  1525  				assert.False(t, needRefresh)
  1526  				app.Status.Sync = v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeUnknown}
  1527  
  1528  				needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour)
  1529  				assert.True(t, needRefresh)
  1530  				assert.Equal(t, v1alpha1.RefreshTypeNormal, refreshType)
  1531  				assert.Equal(t, CompareWithLatestForceResolve, compareWith)
  1532  			})
  1533  
  1534  			t.Run("refresh app using the 'latest' level if comparison expired", func(t *testing.T) {
  1535  				app := app.DeepCopy()
  1536  
  1537  				// use a one-off controller so other tests don't have a manual refresh request
  1538  				ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil)
  1539  
  1540  				needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour)
  1541  				assert.False(t, needRefresh)
  1542  
  1543  				ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), nil)
  1544  				reconciledAt := metav1.NewTime(time.Now().UTC().Add(-1 * time.Hour))
  1545  				app.Status.ReconciledAt = &reconciledAt
  1546  				needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 1*time.Minute, 2*time.Hour)
  1547  				assert.True(t, needRefresh)
  1548  				assert.Equal(t, v1alpha1.RefreshTypeNormal, refreshType)
  1549  				assert.Equal(t, CompareWithLatest, compareWith)
  1550  			})
  1551  
  1552  			t.Run("refresh app using the 'latest' level if comparison expired for hard refresh", func(t *testing.T) {
  1553  				app := app.DeepCopy()
  1554  				app.Status.Sync = v1alpha1.SyncStatus{
  1555  					Status: v1alpha1.SyncStatusCodeSynced,
  1556  					ComparedTo: v1alpha1.ComparedTo{
  1557  						Destination:       app.Spec.Destination,
  1558  						IgnoreDifferences: app.Spec.IgnoreDifferences,
  1559  					},
  1560  				}
  1561  				if app.Spec.HasMultipleSources() {
  1562  					app.Status.Sync.ComparedTo.Sources = app.Spec.Sources
  1563  				} else {
  1564  					app.Status.Sync.ComparedTo.Source = app.Spec.GetSource()
  1565  				}
  1566  
  1567  				// use a one-off controller so other tests don't have a manual refresh request
  1568  				ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil)
  1569  
  1570  				needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour)
  1571  				assert.False(t, needRefresh)
  1572  				ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), nil)
  1573  				reconciledAt := metav1.NewTime(time.Now().UTC().Add(-1 * time.Hour))
  1574  				app.Status.ReconciledAt = &reconciledAt
  1575  				needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 2*time.Hour, 1*time.Minute)
  1576  				assert.True(t, needRefresh)
  1577  				assert.Equal(t, v1alpha1.RefreshTypeHard, refreshType)
  1578  				assert.Equal(t, CompareWithLatest, compareWith)
  1579  			})
  1580  
  1581  			t.Run("execute hard refresh if app has refresh annotation", func(t *testing.T) {
  1582  				app := app.DeepCopy()
  1583  				needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour)
  1584  				assert.False(t, needRefresh)
  1585  				reconciledAt := metav1.NewTime(time.Now().UTC().Add(-1 * time.Hour))
  1586  				app.Status.ReconciledAt = &reconciledAt
  1587  				app.Annotations = map[string]string{
  1588  					v1alpha1.AnnotationKeyRefresh: string(v1alpha1.RefreshTypeHard),
  1589  				}
  1590  				needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour)
  1591  				assert.True(t, needRefresh)
  1592  				assert.Equal(t, v1alpha1.RefreshTypeHard, refreshType)
  1593  				assert.Equal(t, CompareWithLatestForceResolve, compareWith)
  1594  			})
  1595  
  1596  			t.Run("ensure that CompareWithLatest level is used if application source has changed", func(t *testing.T) {
  1597  				app := app.DeepCopy()
  1598  				needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour)
  1599  				assert.False(t, needRefresh)
  1600  				// sample app source change
  1601  				if app.Spec.HasMultipleSources() {
  1602  					app.Spec.Sources[0].Helm = &v1alpha1.ApplicationSourceHelm{
  1603  						Parameters: []v1alpha1.HelmParameter{{
  1604  							Name:  "foo",
  1605  							Value: "bar",
  1606  						}},
  1607  					}
  1608  				} else {
  1609  					app.Spec.Source.Helm = &v1alpha1.ApplicationSourceHelm{
  1610  						Parameters: []v1alpha1.HelmParameter{{
  1611  							Name:  "foo",
  1612  							Value: "bar",
  1613  						}},
  1614  					}
  1615  				}
  1616  
  1617  				needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour)
  1618  				assert.True(t, needRefresh)
  1619  				assert.Equal(t, v1alpha1.RefreshTypeNormal, refreshType)
  1620  				assert.Equal(t, CompareWithLatestForceResolve, compareWith)
  1621  			})
  1622  
  1623  			t.Run("ensure that CompareWithLatest level is used if ignored differences change", func(t *testing.T) {
  1624  				app := app.DeepCopy()
  1625  				needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour)
  1626  				assert.False(t, needRefresh)
  1627  
  1628  				app.Spec.IgnoreDifferences = []v1alpha1.ResourceIgnoreDifferences{
  1629  					{
  1630  						Group: "apps",
  1631  						Kind:  "Deployment",
  1632  						JSONPointers: []string{
  1633  							"/spec/template/spec/containers/0/image",
  1634  						},
  1635  					},
  1636  				}
  1637  
  1638  				needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 1*time.Hour, 2*time.Hour)
  1639  				assert.True(t, needRefresh)
  1640  				assert.Equal(t, v1alpha1.RefreshTypeNormal, refreshType)
  1641  				assert.Equal(t, CompareWithLatest, compareWith)
  1642  			})
  1643  		})
  1644  	}
  1645  }
  1646  
  1647  func TestUpdatedManagedNamespaceMetadata(t *testing.T) {
  1648  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil)
  1649  	app := newFakeApp()
  1650  	app.Spec.SyncPolicy.ManagedNamespaceMetadata = &v1alpha1.ManagedNamespaceMetadata{
  1651  		Labels: map[string]string{
  1652  			"foo": "bar",
  1653  		},
  1654  		Annotations: map[string]string{
  1655  			"foo": "bar",
  1656  		},
  1657  	}
  1658  	app.Status.Sync.ComparedTo.Source = app.Spec.GetSource()
  1659  	app.Status.Sync.ComparedTo.Destination = app.Spec.Destination
  1660  
  1661  	// Ensure that hard/soft refresh isn't triggered due to reconciledAt being expired
  1662  	reconciledAt := metav1.NewTime(time.Now().UTC().Add(15 * time.Minute))
  1663  	app.Status.ReconciledAt = &reconciledAt
  1664  	needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 30*time.Minute, 2*time.Hour)
  1665  
  1666  	assert.True(t, needRefresh)
  1667  	assert.Equal(t, v1alpha1.RefreshTypeNormal, refreshType)
  1668  	assert.Equal(t, CompareWithLatest, compareWith)
  1669  }
  1670  
  1671  func TestUnchangedManagedNamespaceMetadata(t *testing.T) {
  1672  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{}}, nil)
  1673  	app := newFakeApp()
  1674  	app.Spec.SyncPolicy.ManagedNamespaceMetadata = &v1alpha1.ManagedNamespaceMetadata{
  1675  		Labels: map[string]string{
  1676  			"foo": "bar",
  1677  		},
  1678  		Annotations: map[string]string{
  1679  			"foo": "bar",
  1680  		},
  1681  	}
  1682  	app.Status.Sync.ComparedTo.Source = app.Spec.GetSource()
  1683  	app.Status.Sync.ComparedTo.Destination = app.Spec.Destination
  1684  	app.Status.OperationState.SyncResult.ManagedNamespaceMetadata = app.Spec.SyncPolicy.ManagedNamespaceMetadata
  1685  
  1686  	// Ensure that hard/soft refresh isn't triggered due to reconciledAt being expired
  1687  	reconciledAt := metav1.NewTime(time.Now().UTC().Add(15 * time.Minute))
  1688  	app.Status.ReconciledAt = &reconciledAt
  1689  	needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 30*time.Minute, 2*time.Hour)
  1690  
  1691  	assert.False(t, needRefresh)
  1692  	assert.Equal(t, v1alpha1.RefreshTypeNormal, refreshType)
  1693  	assert.Equal(t, CompareWithLatest, compareWith)
  1694  }
  1695  
  1696  func TestRefreshAppConditions(t *testing.T) {
  1697  	defaultProj := v1alpha1.AppProject{
  1698  		ObjectMeta: metav1.ObjectMeta{
  1699  			Name:      "default",
  1700  			Namespace: test.FakeArgoCDNamespace,
  1701  		},
  1702  		Spec: v1alpha1.AppProjectSpec{
  1703  			SourceRepos: []string{"*"},
  1704  			Destinations: []v1alpha1.ApplicationDestination{
  1705  				{
  1706  					Server:    "*",
  1707  					Namespace: "*",
  1708  				},
  1709  			},
  1710  		},
  1711  	}
  1712  
  1713  	t.Run("NoErrorConditions", func(t *testing.T) {
  1714  		app := newFakeApp()
  1715  		ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}}, nil)
  1716  
  1717  		_, hasErrors := ctrl.refreshAppConditions(app)
  1718  		assert.False(t, hasErrors)
  1719  		assert.Empty(t, app.Status.Conditions)
  1720  	})
  1721  
  1722  	t.Run("PreserveExistingWarningCondition", func(t *testing.T) {
  1723  		app := newFakeApp()
  1724  		app.Status.SetConditions([]v1alpha1.ApplicationCondition{{Type: v1alpha1.ApplicationConditionExcludedResourceWarning}}, nil)
  1725  
  1726  		ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}}, nil)
  1727  
  1728  		_, hasErrors := ctrl.refreshAppConditions(app)
  1729  		assert.False(t, hasErrors)
  1730  		assert.Len(t, app.Status.Conditions, 1)
  1731  		assert.Equal(t, v1alpha1.ApplicationConditionExcludedResourceWarning, app.Status.Conditions[0].Type)
  1732  	})
  1733  
  1734  	t.Run("ReplacesSpecErrorCondition", func(t *testing.T) {
  1735  		app := newFakeApp()
  1736  		app.Spec.Project = "wrong project"
  1737  		app.Status.SetConditions([]v1alpha1.ApplicationCondition{{Type: v1alpha1.ApplicationConditionInvalidSpecError, Message: "old message"}}, nil)
  1738  
  1739  		ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}}, nil)
  1740  
  1741  		_, hasErrors := ctrl.refreshAppConditions(app)
  1742  		assert.True(t, hasErrors)
  1743  		assert.Len(t, app.Status.Conditions, 1)
  1744  		assert.Equal(t, v1alpha1.ApplicationConditionInvalidSpecError, app.Status.Conditions[0].Type)
  1745  		assert.Equal(t, "Application referencing project wrong project which does not exist", app.Status.Conditions[0].Message)
  1746  	})
  1747  }
  1748  
  1749  func TestUpdateReconciledAt(t *testing.T) {
  1750  	app := newFakeApp()
  1751  	reconciledAt := metav1.NewTime(time.Now().Add(-1 * time.Second))
  1752  	app.Status = v1alpha1.ApplicationStatus{ReconciledAt: &reconciledAt}
  1753  	app.Status.Sync = v1alpha1.SyncStatus{ComparedTo: v1alpha1.ComparedTo{Source: app.Spec.GetSource(), Destination: app.Spec.Destination, IgnoreDifferences: app.Spec.IgnoreDifferences}}
  1754  	ctrl := newFakeController(&fakeData{
  1755  		apps: []runtime.Object{app, &defaultProj},
  1756  		manifestResponse: &apiclient.ManifestResponse{
  1757  			Manifests: []string{},
  1758  			Namespace: test.FakeDestNamespace,
  1759  			Server:    test.FakeClusterURL,
  1760  			Revision:  "abc123",
  1761  		},
  1762  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
  1763  	}, nil)
  1764  	key, _ := cache.MetaNamespaceKeyFunc(app)
  1765  	fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  1766  	fakeAppCs.ReactionChain = nil
  1767  	receivedPatch := map[string]any{}
  1768  	fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  1769  		if patchAction, ok := action.(kubetesting.PatchAction); ok {
  1770  			require.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
  1771  		}
  1772  		return true, &v1alpha1.Application{}, nil
  1773  	})
  1774  
  1775  	t.Run("UpdatedOnFullReconciliation", func(t *testing.T) {
  1776  		receivedPatch = map[string]any{}
  1777  		ctrl.requestAppRefresh(app.Name, CompareWithLatest.Pointer(), nil)
  1778  		ctrl.appRefreshQueue.AddRateLimited(key)
  1779  
  1780  		ctrl.processAppRefreshQueueItem()
  1781  
  1782  		_, updated, err := unstructured.NestedString(receivedPatch, "status", "reconciledAt")
  1783  		require.NoError(t, err)
  1784  		assert.True(t, updated)
  1785  
  1786  		_, updated, err = unstructured.NestedString(receivedPatch, "status", "observedAt")
  1787  		require.NoError(t, err)
  1788  		assert.False(t, updated)
  1789  	})
  1790  
  1791  	t.Run("NotUpdatedOnPartialReconciliation", func(t *testing.T) {
  1792  		receivedPatch = map[string]any{}
  1793  		ctrl.appRefreshQueue.AddRateLimited(key)
  1794  		ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), nil)
  1795  
  1796  		ctrl.processAppRefreshQueueItem()
  1797  
  1798  		_, updated, err := unstructured.NestedString(receivedPatch, "status", "reconciledAt")
  1799  		require.NoError(t, err)
  1800  		assert.False(t, updated)
  1801  
  1802  		_, updated, err = unstructured.NestedString(receivedPatch, "status", "observedAt")
  1803  		require.NoError(t, err)
  1804  		assert.False(t, updated)
  1805  	})
  1806  }
  1807  
  1808  func TestUpdateHealthStatusTransitionTime(t *testing.T) {
  1809  	deployment := kube.MustToUnstructured(&appsv1.Deployment{
  1810  		TypeMeta: metav1.TypeMeta{
  1811  			APIVersion: "apps/v1",
  1812  			Kind:       "Deployment",
  1813  		},
  1814  		ObjectMeta: metav1.ObjectMeta{
  1815  			Name:      "demo",
  1816  			Namespace: "default",
  1817  		},
  1818  	})
  1819  	testCases := []struct {
  1820  		name           string
  1821  		app            *v1alpha1.Application
  1822  		configMapData  map[string]string
  1823  		expectedStatus health.HealthStatusCode
  1824  	}{
  1825  		{
  1826  			name: "Degraded to Missing",
  1827  			app:  newFakeAppWithHealthAndTime(health.HealthStatusDegraded, testTimestamp),
  1828  			configMapData: map[string]string{
  1829  				"resource.customizations": `
  1830  apps/Deployment:
  1831    health.lua: |
  1832      hs = {}
  1833      hs.status = "Missing"
  1834      hs.message = ""
  1835      return hs`,
  1836  			},
  1837  			expectedStatus: health.HealthStatusMissing,
  1838  		},
  1839  		{
  1840  			name: "Missing to Progressing",
  1841  			app:  newFakeAppWithHealthAndTime(health.HealthStatusMissing, testTimestamp),
  1842  			configMapData: map[string]string{
  1843  				"resource.customizations": `
  1844  apps/Deployment:
  1845    health.lua: |
  1846      hs = {}
  1847      hs.status = "Progressing"
  1848      hs.message = ""
  1849      return hs`,
  1850  			},
  1851  			expectedStatus: health.HealthStatusProgressing,
  1852  		},
  1853  		{
  1854  			name: "Progressing to Healthy",
  1855  			app:  newFakeAppWithHealthAndTime(health.HealthStatusProgressing, testTimestamp),
  1856  			configMapData: map[string]string{
  1857  				"resource.customizations": `
  1858  apps/Deployment:
  1859    health.lua: |
  1860      hs = {}
  1861      hs.status = "Healthy"
  1862      hs.message = ""
  1863      return hs`,
  1864  			},
  1865  			expectedStatus: health.HealthStatusHealthy,
  1866  		},
  1867  		{
  1868  			name: "Healthy  to Degraded",
  1869  			app:  newFakeAppWithHealthAndTime(health.HealthStatusHealthy, testTimestamp),
  1870  			configMapData: map[string]string{
  1871  				"resource.customizations": `
  1872  apps/Deployment:
  1873    health.lua: |
  1874      hs = {}
  1875      hs.status = "Degraded"
  1876      hs.message = ""
  1877      return hs`,
  1878  			},
  1879  			expectedStatus: health.HealthStatusDegraded,
  1880  		},
  1881  	}
  1882  
  1883  	for _, tc := range testCases {
  1884  		t.Run(tc.name, func(t *testing.T) {
  1885  			ctrl := newFakeController(&fakeData{
  1886  				apps: []runtime.Object{tc.app, &defaultProj},
  1887  				manifestResponse: &apiclient.ManifestResponse{
  1888  					Manifests: []string{},
  1889  					Namespace: test.FakeDestNamespace,
  1890  					Server:    test.FakeClusterURL,
  1891  					Revision:  "abc123",
  1892  				},
  1893  				managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
  1894  					kube.GetResourceKey(deployment): deployment,
  1895  				},
  1896  				configMapData: tc.configMapData,
  1897  			}, nil)
  1898  
  1899  			ctrl.processAppRefreshQueueItem()
  1900  			apps, err := ctrl.appLister.List(labels.Everything())
  1901  			require.NoError(t, err)
  1902  			assert.NotEmpty(t, apps)
  1903  			assert.Equal(t, tc.expectedStatus, apps[0].Status.Health.Status)
  1904  			assert.NotEqual(t, testTimestamp, *apps[0].Status.Health.LastTransitionTime)
  1905  		})
  1906  	}
  1907  }
  1908  
  1909  func TestUpdateHealthStatusProgression(t *testing.T) {
  1910  	app := newFakeAppWithHealthAndTime(health.HealthStatusDegraded, testTimestamp)
  1911  	deployment := kube.MustToUnstructured(&appsv1.Deployment{
  1912  		TypeMeta: metav1.TypeMeta{
  1913  			APIVersion: "apps/v1",
  1914  			Kind:       "Deployment",
  1915  		},
  1916  		ObjectMeta: metav1.ObjectMeta{
  1917  			Name:      "demo",
  1918  			Namespace: "default",
  1919  		},
  1920  		Status: appsv1.DeploymentStatus{
  1921  			ObservedGeneration: 0,
  1922  		},
  1923  	})
  1924  	configMapData := map[string]string{
  1925  		"resource.customizations": `
  1926  apps/Deployment:
  1927    health.lua: |
  1928      hs = {}
  1929      hs.status = ""
  1930      hs.message = ""
  1931  
  1932      if obj.metadata ~= nil then
  1933        if obj.metadata.labels ~= nil then
  1934          current_status = obj.metadata.labels["status"]
  1935          if current_status == "Degraded" then
  1936            hs.status = "Missing"
  1937          elseif current_status == "Missing" then
  1938            hs.status = "Progressing"
  1939          elseif current_status == "Progressing" then
  1940            hs.status = "Healthy"
  1941          elseif current_status == "Healthy" then
  1942            hs.status = "Degraded"
  1943          end
  1944        end
  1945      end
  1946  
  1947      return hs`,
  1948  	}
  1949  	ctrl := newFakeControllerWithResync(&fakeData{
  1950  		apps: []runtime.Object{app, &defaultProj},
  1951  		manifestResponse: &apiclient.ManifestResponse{
  1952  			Manifests: []string{},
  1953  			Namespace: test.FakeDestNamespace,
  1954  			Server:    test.FakeClusterURL,
  1955  			Revision:  "abc123",
  1956  		},
  1957  		managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
  1958  			kube.GetResourceKey(deployment): deployment,
  1959  		},
  1960  		configMapData: configMapData,
  1961  		manifestResponses: []*apiclient.ManifestResponse{
  1962  			{},
  1963  			{},
  1964  			{},
  1965  			{},
  1966  		},
  1967  	}, time.Millisecond*10, nil, nil)
  1968  
  1969  	testCases := []struct {
  1970  		name           string
  1971  		initialStatus  string
  1972  		expectedStatus health.HealthStatusCode
  1973  	}{
  1974  		{
  1975  			name:           "Degraded to Missing",
  1976  			initialStatus:  "Degraded",
  1977  			expectedStatus: health.HealthStatusMissing,
  1978  		},
  1979  		{
  1980  			name:           "Missing to Progressing",
  1981  			initialStatus:  "Missing",
  1982  			expectedStatus: health.HealthStatusProgressing,
  1983  		},
  1984  		{
  1985  			name:           "Progressing to Healthy",
  1986  			initialStatus:  "Progressing",
  1987  			expectedStatus: health.HealthStatusHealthy,
  1988  		},
  1989  		{
  1990  			name:           "Healthy to Degraded",
  1991  			initialStatus:  "Healthy",
  1992  			expectedStatus: health.HealthStatusDegraded,
  1993  		},
  1994  	}
  1995  
  1996  	for _, tc := range testCases {
  1997  		t.Run(tc.name, func(t *testing.T) {
  1998  			deployment.SetLabels(map[string]string{"status": tc.initialStatus})
  1999  			ctrl.processAppRefreshQueueItem()
  2000  			apps, err := ctrl.appLister.List(labels.Everything())
  2001  			require.NoError(t, err)
  2002  			if assert.NotEmpty(t, apps) {
  2003  				assert.Equal(t, tc.expectedStatus, apps[0].Status.Health.Status)
  2004  				assert.NotEqual(t, testTimestamp, *apps[0].Status.Health.LastTransitionTime)
  2005  			}
  2006  
  2007  			ctrl.requestAppRefresh(app.Name, nil, nil)
  2008  			time.Sleep(time.Millisecond * 15)
  2009  		})
  2010  	}
  2011  }
  2012  
  2013  func TestProjectErrorToCondition(t *testing.T) {
  2014  	app := newFakeApp()
  2015  	app.Spec.Project = "wrong project"
  2016  	ctrl := newFakeController(&fakeData{
  2017  		apps: []runtime.Object{app, &defaultProj},
  2018  		manifestResponse: &apiclient.ManifestResponse{
  2019  			Manifests: []string{},
  2020  			Namespace: test.FakeDestNamespace,
  2021  			Server:    test.FakeClusterURL,
  2022  			Revision:  "abc123",
  2023  		},
  2024  		managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
  2025  	}, nil)
  2026  	key, _ := cache.MetaNamespaceKeyFunc(app)
  2027  	ctrl.appRefreshQueue.AddRateLimited(key)
  2028  	ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), nil)
  2029  
  2030  	ctrl.processAppRefreshQueueItem()
  2031  
  2032  	obj, ok, err := ctrl.appInformer.GetIndexer().GetByKey(key)
  2033  	assert.True(t, ok)
  2034  	require.NoError(t, err)
  2035  	updatedApp := obj.(*v1alpha1.Application)
  2036  	assert.Equal(t, v1alpha1.ApplicationConditionInvalidSpecError, updatedApp.Status.Conditions[0].Type)
  2037  	assert.Equal(t, "Application referencing project wrong project which does not exist", updatedApp.Status.Conditions[0].Message)
  2038  	assert.Equal(t, v1alpha1.ApplicationConditionInvalidSpecError, updatedApp.Status.Conditions[0].Type)
  2039  }
  2040  
  2041  func TestFinalizeProjectDeletion_HasApplications(t *testing.T) {
  2042  	app := newFakeApp()
  2043  	proj := &v1alpha1.AppProject{ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: test.FakeArgoCDNamespace}}
  2044  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, proj}}, nil)
  2045  
  2046  	fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  2047  	patched := false
  2048  	fakeAppCs.PrependReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  2049  		patched = true
  2050  		return true, &v1alpha1.Application{}, nil
  2051  	})
  2052  
  2053  	err := ctrl.finalizeProjectDeletion(proj)
  2054  	require.NoError(t, err)
  2055  	assert.False(t, patched)
  2056  }
  2057  
  2058  func TestFinalizeProjectDeletion_DoesNotHaveApplications(t *testing.T) {
  2059  	proj := &v1alpha1.AppProject{ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: test.FakeArgoCDNamespace}}
  2060  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{&defaultProj}}, nil)
  2061  
  2062  	fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  2063  	receivedPatch := map[string]any{}
  2064  	fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  2065  		if patchAction, ok := action.(kubetesting.PatchAction); ok {
  2066  			require.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
  2067  		}
  2068  		return true, &v1alpha1.AppProject{}, nil
  2069  	})
  2070  
  2071  	err := ctrl.finalizeProjectDeletion(proj)
  2072  	require.NoError(t, err)
  2073  	assert.Equal(t, map[string]any{
  2074  		"metadata": map[string]any{
  2075  			"finalizers": nil,
  2076  		},
  2077  	}, receivedPatch)
  2078  }
  2079  
  2080  func TestProcessRequestedAppOperation_FailedNoRetries(t *testing.T) {
  2081  	app := newFakeApp()
  2082  	app.Spec.Project = "default"
  2083  	app.Operation = &v1alpha1.Operation{
  2084  		Sync: &v1alpha1.SyncOperation{},
  2085  	}
  2086  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
  2087  	fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  2088  	receivedPatch := map[string]any{}
  2089  	fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  2090  		if patchAction, ok := action.(kubetesting.PatchAction); ok {
  2091  			require.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
  2092  		}
  2093  		return true, &v1alpha1.Application{}, nil
  2094  	})
  2095  
  2096  	ctrl.processRequestedAppOperation(app)
  2097  
  2098  	phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
  2099  	message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message")
  2100  	assert.Equal(t, string(synccommon.OperationError), phase)
  2101  	assert.Equal(t, "Failed to load application project: error getting app project \"default\": appproject.argoproj.io \"default\" not found", message)
  2102  }
  2103  
  2104  func TestProcessRequestedAppOperation_InvalidDestination(t *testing.T) {
  2105  	app := newFakeAppWithDestMismatch()
  2106  	app.Spec.Project = "test-project"
  2107  	app.Operation = &v1alpha1.Operation{
  2108  		Sync: &v1alpha1.SyncOperation{},
  2109  	}
  2110  	proj := defaultProj
  2111  	proj.Name = "test-project"
  2112  	proj.Spec.SourceNamespaces = []string{test.FakeArgoCDNamespace}
  2113  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &proj}}, nil)
  2114  	fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  2115  	receivedPatch := map[string]any{}
  2116  	func() {
  2117  		fakeAppCs.Lock()
  2118  		defer fakeAppCs.Unlock()
  2119  		fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  2120  			if patchAction, ok := action.(kubetesting.PatchAction); ok {
  2121  				require.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
  2122  			}
  2123  			return true, &v1alpha1.Application{}, nil
  2124  		})
  2125  	}()
  2126  
  2127  	ctrl.processRequestedAppOperation(app)
  2128  
  2129  	phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
  2130  	message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message")
  2131  	assert.Equal(t, string(synccommon.OperationError), phase)
  2132  	assert.Contains(t, message, "application destination can't have both name and server defined: another-cluster https://localhost:6443")
  2133  }
  2134  
  2135  func TestProcessRequestedAppOperation_FailedHasRetries(t *testing.T) {
  2136  	app := newFakeApp()
  2137  	app.Spec.Project = "invalid-project"
  2138  	app.Operation = &v1alpha1.Operation{
  2139  		Sync:  &v1alpha1.SyncOperation{},
  2140  		Retry: v1alpha1.RetryStrategy{Limit: 1},
  2141  	}
  2142  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
  2143  	fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  2144  	receivedPatch := map[string]any{}
  2145  	fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  2146  		if patchAction, ok := action.(kubetesting.PatchAction); ok {
  2147  			require.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
  2148  		}
  2149  		return true, &v1alpha1.Application{}, nil
  2150  	})
  2151  
  2152  	ctrl.processRequestedAppOperation(app)
  2153  
  2154  	phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
  2155  	message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message")
  2156  	retryCount, _, _ := unstructured.NestedFloat64(receivedPatch, "status", "operationState", "retryCount")
  2157  	assert.Equal(t, string(synccommon.OperationRunning), phase)
  2158  	assert.Contains(t, message, "Failed to load application project: error getting app project \"invalid-project\": appproject.argoproj.io \"invalid-project\" not found. Retrying attempt #1")
  2159  	assert.InEpsilon(t, float64(1), retryCount, 0.0001)
  2160  }
  2161  
  2162  func TestProcessRequestedAppOperation_RunningPreviouslyFailed(t *testing.T) {
  2163  	failedAttemptFinisedAt := time.Now().Add(-time.Minute * 5)
  2164  	app := newFakeApp()
  2165  	app.Operation = &v1alpha1.Operation{
  2166  		Sync:  &v1alpha1.SyncOperation{},
  2167  		Retry: v1alpha1.RetryStrategy{Limit: 1},
  2168  	}
  2169  	app.Status.OperationState.Operation = *app.Operation
  2170  	app.Status.OperationState.Phase = synccommon.OperationRunning
  2171  	app.Status.OperationState.RetryCount = 1
  2172  	app.Status.OperationState.FinishedAt = &metav1.Time{Time: failedAttemptFinisedAt}
  2173  	app.Status.OperationState.SyncResult.Resources = []*v1alpha1.ResourceResult{{
  2174  		Name:   "guestbook",
  2175  		Kind:   "Deployment",
  2176  		Group:  "apps",
  2177  		Status: synccommon.ResultCodeSyncFailed,
  2178  	}}
  2179  
  2180  	data := &fakeData{
  2181  		apps: []runtime.Object{app, &defaultProj},
  2182  		manifestResponse: &apiclient.ManifestResponse{
  2183  			Manifests: []string{},
  2184  			Namespace: test.FakeDestNamespace,
  2185  			Server:    test.FakeClusterURL,
  2186  			Revision:  "abc123",
  2187  		},
  2188  	}
  2189  	ctrl := newFakeController(data, nil)
  2190  	fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  2191  	receivedPatch := map[string]any{}
  2192  	fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  2193  		if patchAction, ok := action.(kubetesting.PatchAction); ok {
  2194  			require.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
  2195  		}
  2196  		return true, &v1alpha1.Application{}, nil
  2197  	})
  2198  
  2199  	ctrl.processRequestedAppOperation(app)
  2200  
  2201  	phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
  2202  	message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message")
  2203  	finishedAtStr, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "finishedAt")
  2204  	finishedAt, err := time.Parse(time.RFC3339, finishedAtStr)
  2205  	require.NoError(t, err)
  2206  	assert.Equal(t, string(synccommon.OperationSucceeded), phase)
  2207  	assert.Equal(t, "successfully synced (no more tasks)", message)
  2208  	assert.Truef(t, finishedAt.After(failedAttemptFinisedAt), "finishedAt was expected to be updated. The retry was not performed.")
  2209  }
  2210  
  2211  func TestProcessRequestedAppOperation_RunningPreviouslyFailedBackoff(t *testing.T) {
  2212  	failedAttemptFinisedAt := time.Now().Add(-time.Second)
  2213  	app := newFakeApp()
  2214  	app.Operation = &v1alpha1.Operation{
  2215  		Sync: &v1alpha1.SyncOperation{},
  2216  		Retry: v1alpha1.RetryStrategy{
  2217  			Limit: 1,
  2218  			Backoff: &v1alpha1.Backoff{
  2219  				Duration:    "1h",
  2220  				Factor:      ptr.To(int64(100)),
  2221  				MaxDuration: "1h",
  2222  			},
  2223  		},
  2224  	}
  2225  	app.Status.OperationState.Operation = *app.Operation
  2226  	app.Status.OperationState.Phase = synccommon.OperationRunning
  2227  	app.Status.OperationState.Message = "pending retry"
  2228  	app.Status.OperationState.RetryCount = 1
  2229  	app.Status.OperationState.FinishedAt = &metav1.Time{Time: failedAttemptFinisedAt}
  2230  	app.Status.OperationState.SyncResult.Resources = []*v1alpha1.ResourceResult{{
  2231  		Name:   "guestbook",
  2232  		Kind:   "Deployment",
  2233  		Group:  "apps",
  2234  		Status: synccommon.ResultCodeSyncFailed,
  2235  	}}
  2236  
  2237  	data := &fakeData{
  2238  		apps: []runtime.Object{app, &defaultProj},
  2239  		manifestResponse: &apiclient.ManifestResponse{
  2240  			Manifests: []string{},
  2241  			Namespace: test.FakeDestNamespace,
  2242  			Server:    test.FakeClusterURL,
  2243  			Revision:  "abc123",
  2244  		},
  2245  	}
  2246  	ctrl := newFakeController(data, nil)
  2247  	fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  2248  	fakeAppCs.PrependReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  2249  		require.FailNow(t, "A patch should not have been called if the backoff has not passed")
  2250  		return true, &v1alpha1.Application{}, nil
  2251  	})
  2252  
  2253  	ctrl.processRequestedAppOperation(app)
  2254  }
  2255  
  2256  func TestProcessRequestedAppOperation_HasRetriesTerminated(t *testing.T) {
  2257  	app := newFakeApp()
  2258  	app.Operation = &v1alpha1.Operation{
  2259  		Sync:  &v1alpha1.SyncOperation{},
  2260  		Retry: v1alpha1.RetryStrategy{Limit: 10},
  2261  	}
  2262  	app.Status.OperationState.Operation = *app.Operation
  2263  	app.Status.OperationState.Phase = synccommon.OperationTerminating
  2264  
  2265  	data := &fakeData{
  2266  		apps: []runtime.Object{app, &defaultProj},
  2267  		manifestResponse: &apiclient.ManifestResponse{
  2268  			Manifests: []string{},
  2269  			Namespace: test.FakeDestNamespace,
  2270  			Server:    test.FakeClusterURL,
  2271  			Revision:  "abc123",
  2272  		},
  2273  	}
  2274  	ctrl := newFakeController(data, nil)
  2275  	fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  2276  	receivedPatch := map[string]any{}
  2277  	fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  2278  		if patchAction, ok := action.(kubetesting.PatchAction); ok {
  2279  			require.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
  2280  		}
  2281  		return true, &v1alpha1.Application{}, nil
  2282  	})
  2283  
  2284  	ctrl.processRequestedAppOperation(app)
  2285  
  2286  	phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
  2287  	message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message")
  2288  	assert.Equal(t, string(synccommon.OperationFailed), phase)
  2289  	assert.Equal(t, "Operation terminated", message)
  2290  }
  2291  
  2292  func TestProcessRequestedAppOperation_Successful(t *testing.T) {
  2293  	app := newFakeApp()
  2294  	app.Spec.Project = "default"
  2295  	app.Operation = &v1alpha1.Operation{
  2296  		Sync: &v1alpha1.SyncOperation{},
  2297  	}
  2298  	ctrl := newFakeController(&fakeData{
  2299  		apps: []runtime.Object{app, &defaultProj},
  2300  		manifestResponses: []*apiclient.ManifestResponse{{
  2301  			Manifests: []string{},
  2302  		}},
  2303  	}, nil)
  2304  	fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
  2305  	receivedPatch := map[string]any{}
  2306  	fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
  2307  		if patchAction, ok := action.(kubetesting.PatchAction); ok {
  2308  			require.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
  2309  		}
  2310  		return true, &v1alpha1.Application{}, nil
  2311  	})
  2312  
  2313  	ctrl.processRequestedAppOperation(app)
  2314  
  2315  	phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
  2316  	message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message")
  2317  	assert.Equal(t, string(synccommon.OperationSucceeded), phase)
  2318  	assert.Equal(t, "successfully synced (no more tasks)", message)
  2319  	ok, level := ctrl.isRefreshRequested(ctrl.toAppKey(app.Name))
  2320  	assert.True(t, ok)
  2321  	assert.Equal(t, CompareWithLatestForceResolve, level)
  2322  }
  2323  
  2324  func TestProcessRequestedAppOperation_SyncTimeout(t *testing.T) {
  2325  	testCases := []struct {
  2326  		name            string
  2327  		startedSince    time.Duration
  2328  		syncTimeout     time.Duration
  2329  		retryAttempt    int
  2330  		currentPhase    synccommon.OperationPhase
  2331  		expectedPhase   synccommon.OperationPhase
  2332  		expectedMessage string
  2333  	}{{
  2334  		name:            "Continue when running operation has not exceeded timeout",
  2335  		syncTimeout:     time.Minute,
  2336  		startedSince:    30 * time.Second,
  2337  		currentPhase:    synccommon.OperationRunning,
  2338  		expectedPhase:   synccommon.OperationSucceeded,
  2339  		expectedMessage: "successfully synced (no more tasks)",
  2340  	}, {
  2341  		name:            "Continue when terminating operation has exceeded timeout",
  2342  		syncTimeout:     time.Minute,
  2343  		startedSince:    2 * time.Minute,
  2344  		currentPhase:    synccommon.OperationTerminating,
  2345  		expectedPhase:   synccommon.OperationFailed,
  2346  		expectedMessage: "Operation terminated",
  2347  	}, {
  2348  		name:            "Terminate when running operation exceeded timeout",
  2349  		syncTimeout:     time.Minute,
  2350  		startedSince:    2 * time.Minute,
  2351  		currentPhase:    synccommon.OperationRunning,
  2352  		expectedPhase:   synccommon.OperationFailed,
  2353  		expectedMessage: "Operation terminated, triggered by controller sync timeout",
  2354  	}, {
  2355  		name:            "Terminate when retried operation exceeded timeout",
  2356  		syncTimeout:     time.Minute,
  2357  		startedSince:    15 * time.Minute,
  2358  		currentPhase:    synccommon.OperationRunning,
  2359  		retryAttempt:    1,
  2360  		expectedPhase:   synccommon.OperationFailed,
  2361  		expectedMessage: "Operation terminated, triggered by controller sync timeout (retried 1 times).",
  2362  	}}
  2363  	for i := range testCases {
  2364  		tc := testCases[i]
  2365  		t.Run(fmt.Sprintf("case %d: %s", i, tc.name), func(t *testing.T) {
  2366  			app := newFakeApp()
  2367  			app.Spec.Project = "default"
  2368  			app.Operation = &v1alpha1.Operation{
  2369  				Sync: &v1alpha1.SyncOperation{
  2370  					Revision: "HEAD",
  2371  				},
  2372  			}
  2373  			ctrl := newFakeController(&fakeData{
  2374  				apps: []runtime.Object{app, &defaultProj},
  2375  				manifestResponses: []*apiclient.ManifestResponse{{
  2376  					Manifests: []string{},
  2377  				}},
  2378  			}, nil)
  2379  
  2380  			ctrl.syncTimeout = tc.syncTimeout
  2381  			app.Status.OperationState = &v1alpha1.OperationState{
  2382  				Operation: *app.Operation,
  2383  				Phase:     tc.currentPhase,
  2384  				StartedAt: metav1.NewTime(time.Now().Add(-tc.startedSince)),
  2385  			}
  2386  			if tc.retryAttempt > 0 {
  2387  				app.Status.OperationState.FinishedAt = ptr.To(metav1.NewTime(time.Now().Add(-tc.startedSince)))
  2388  				app.Status.OperationState.RetryCount = int64(tc.retryAttempt)
  2389  			}
  2390  
  2391  			ctrl.processRequestedAppOperation(app)
  2392  
  2393  			app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.ObjectMeta.Namespace).Get(t.Context(), app.Name, metav1.GetOptions{})
  2394  			require.NoError(t, err)
  2395  			assert.Equal(t, tc.expectedPhase, app.Status.OperationState.Phase)
  2396  			assert.Equal(t, tc.expectedMessage, app.Status.OperationState.Message)
  2397  		})
  2398  	}
  2399  }
  2400  
  2401  func TestGetAppHosts(t *testing.T) {
  2402  	app := newFakeApp()
  2403  	data := &fakeData{
  2404  		apps: []runtime.Object{app, &defaultProj},
  2405  		manifestResponse: &apiclient.ManifestResponse{
  2406  			Manifests: []string{},
  2407  			Namespace: test.FakeDestNamespace,
  2408  			Server:    test.FakeClusterURL,
  2409  			Revision:  "abc123",
  2410  		},
  2411  		configMapData: map[string]string{
  2412  			"application.allowedNodeLabels": "label1,label2",
  2413  		},
  2414  	}
  2415  	ctrl := newFakeController(data, nil)
  2416  	mockStateCache := &mockstatecache.LiveStateCache{}
  2417  	mockStateCache.On("IterateResources", mock.Anything, mock.MatchedBy(func(callback func(res *clustercache.Resource, info *statecache.ResourceInfo)) bool {
  2418  		// node resource
  2419  		callback(&clustercache.Resource{
  2420  			Ref: corev1.ObjectReference{Name: "minikube", Kind: "Node", APIVersion: "v1"},
  2421  		}, &statecache.ResourceInfo{NodeInfo: &statecache.NodeInfo{
  2422  			Name:       "minikube",
  2423  			SystemInfo: corev1.NodeSystemInfo{OSImage: "debian"},
  2424  			Capacity:   map[corev1.ResourceName]resource.Quantity{corev1.ResourceCPU: resource.MustParse("5")},
  2425  			Labels:     map[string]string{"label1": "value1", "label2": "value2"},
  2426  		}})
  2427  
  2428  		// app pod
  2429  		callback(&clustercache.Resource{
  2430  			Ref: corev1.ObjectReference{Name: "pod1", Kind: kube.PodKind, APIVersion: "v1", Namespace: "default"},
  2431  		}, &statecache.ResourceInfo{PodInfo: &statecache.PodInfo{
  2432  			NodeName:         "minikube",
  2433  			ResourceRequests: map[corev1.ResourceName]resource.Quantity{corev1.ResourceCPU: resource.MustParse("1")},
  2434  		}})
  2435  		// neighbor pod
  2436  		callback(&clustercache.Resource{
  2437  			Ref: corev1.ObjectReference{Name: "pod2", Kind: kube.PodKind, APIVersion: "v1", Namespace: "default"},
  2438  		}, &statecache.ResourceInfo{PodInfo: &statecache.PodInfo{
  2439  			NodeName:         "minikube",
  2440  			ResourceRequests: map[corev1.ResourceName]resource.Quantity{corev1.ResourceCPU: resource.MustParse("2")},
  2441  		}})
  2442  		return true
  2443  	})).Return(nil)
  2444  	ctrl.stateCache = mockStateCache
  2445  
  2446  	hosts, err := ctrl.getAppHosts(&v1alpha1.Cluster{Server: "test", Name: "test"}, app, []v1alpha1.ResourceNode{{
  2447  		ResourceRef: v1alpha1.ResourceRef{Name: "pod1", Namespace: "default", Kind: kube.PodKind},
  2448  		Info: []v1alpha1.InfoItem{{
  2449  			Name:  "Host",
  2450  			Value: "Minikube",
  2451  		}},
  2452  	}})
  2453  
  2454  	require.NoError(t, err)
  2455  	assert.Equal(t, []v1alpha1.HostInfo{{
  2456  		Name:       "minikube",
  2457  		SystemInfo: corev1.NodeSystemInfo{OSImage: "debian"},
  2458  		ResourcesInfo: []v1alpha1.HostResourceInfo{
  2459  			{
  2460  				ResourceName: corev1.ResourceCPU, Capacity: 5000, RequestedByApp: 1000, RequestedByNeighbors: 2000,
  2461  			},
  2462  		},
  2463  		Labels: map[string]string{"label1": "value1", "label2": "value2"},
  2464  	}}, hosts)
  2465  }
  2466  
  2467  func TestMetricsExpiration(t *testing.T) {
  2468  	app := newFakeApp()
  2469  	// Check expiration is disabled by default
  2470  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
  2471  	assert.False(t, ctrl.metricsServer.HasExpiration())
  2472  	// Check expiration is enabled if set
  2473  	ctrl = newFakeController(&fakeData{apps: []runtime.Object{app}, metricsCacheExpiration: 10 * time.Second}, nil)
  2474  	assert.True(t, ctrl.metricsServer.HasExpiration())
  2475  }
  2476  
  2477  func TestToAppKey(t *testing.T) {
  2478  	ctrl := newFakeController(&fakeData{}, nil)
  2479  	tests := []struct {
  2480  		name     string
  2481  		input    string
  2482  		expected string
  2483  	}{
  2484  		{"From instance name", "foo_bar", "foo/bar"},
  2485  		{"From qualified name", "foo/bar", "foo/bar"},
  2486  		{"From unqualified name", "bar", ctrl.namespace + "/bar"},
  2487  	}
  2488  
  2489  	for _, tt := range tests {
  2490  		t.Run(tt.name, func(t *testing.T) {
  2491  			assert.Equal(t, tt.expected, ctrl.toAppKey(tt.input))
  2492  		})
  2493  	}
  2494  }
  2495  
  2496  func Test_canProcessApp(t *testing.T) {
  2497  	app := newFakeApp()
  2498  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
  2499  	ctrl.applicationNamespaces = []string{"good"}
  2500  	t.Run("without cluster filter, good namespace", func(t *testing.T) {
  2501  		app.Namespace = "good"
  2502  		canProcess := ctrl.canProcessApp(app)
  2503  		assert.True(t, canProcess)
  2504  	})
  2505  	t.Run("without cluster filter, bad namespace", func(t *testing.T) {
  2506  		app.Namespace = "bad"
  2507  		canProcess := ctrl.canProcessApp(app)
  2508  		assert.False(t, canProcess)
  2509  	})
  2510  	t.Run("with cluster filter, good namespace", func(t *testing.T) {
  2511  		app.Namespace = "good"
  2512  		canProcess := ctrl.canProcessApp(app)
  2513  		assert.True(t, canProcess)
  2514  	})
  2515  	t.Run("with cluster filter, bad namespace", func(t *testing.T) {
  2516  		app.Namespace = "bad"
  2517  		canProcess := ctrl.canProcessApp(app)
  2518  		assert.False(t, canProcess)
  2519  	})
  2520  }
  2521  
  2522  func Test_canProcessAppSkipReconcileAnnotation(t *testing.T) {
  2523  	appSkipReconcileInvalid := newFakeApp()
  2524  	appSkipReconcileInvalid.Annotations = map[string]string{common.AnnotationKeyAppSkipReconcile: "invalid-value"}
  2525  	appSkipReconcileFalse := newFakeApp()
  2526  	appSkipReconcileFalse.Annotations = map[string]string{common.AnnotationKeyAppSkipReconcile: "false"}
  2527  	appSkipReconcileTrue := newFakeApp()
  2528  	appSkipReconcileTrue.Annotations = map[string]string{common.AnnotationKeyAppSkipReconcile: "true"}
  2529  	ctrl := newFakeController(&fakeData{}, nil)
  2530  	tests := []struct {
  2531  		name     string
  2532  		input    any
  2533  		expected bool
  2534  	}{
  2535  		{"No skip reconcile annotation", newFakeApp(), true},
  2536  		{"Contains skip reconcile annotation ", appSkipReconcileInvalid, true},
  2537  		{"Contains skip reconcile annotation value false", appSkipReconcileFalse, true},
  2538  		{"Contains skip reconcile annotation value true", appSkipReconcileTrue, false},
  2539  	}
  2540  
  2541  	for _, tt := range tests {
  2542  		t.Run(tt.name, func(t *testing.T) {
  2543  			assert.Equal(t, tt.expected, ctrl.canProcessApp(tt.input))
  2544  		})
  2545  	}
  2546  }
  2547  
  2548  func Test_syncDeleteOption(t *testing.T) {
  2549  	app := newFakeApp()
  2550  	ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil)
  2551  	cm := newFakeCM()
  2552  	t.Run("without delete option object is deleted", func(t *testing.T) {
  2553  		cmObj := kube.MustToUnstructured(&cm)
  2554  		assert.True(t, ctrl.shouldBeDeleted(app, cmObj))
  2555  	})
  2556  	t.Run("with delete set to false object is retained", func(t *testing.T) {
  2557  		cmObj := kube.MustToUnstructured(&cm)
  2558  		cmObj.SetAnnotations(map[string]string{"argocd.argoproj.io/sync-options": "Delete=false"})
  2559  		assert.False(t, ctrl.shouldBeDeleted(app, cmObj))
  2560  	})
  2561  	t.Run("with delete set to false object is retained", func(t *testing.T) {
  2562  		cmObj := kube.MustToUnstructured(&cm)
  2563  		cmObj.SetAnnotations(map[string]string{"helm.sh/resource-policy": "keep"})
  2564  		assert.False(t, ctrl.shouldBeDeleted(app, cmObj))
  2565  	})
  2566  }
  2567  
  2568  func TestAddControllerNamespace(t *testing.T) {
  2569  	t.Run("set controllerNamespace when the app is in the controller namespace", func(t *testing.T) {
  2570  		app := newFakeApp()
  2571  		ctrl := newFakeController(&fakeData{
  2572  			apps:             []runtime.Object{app, &defaultProj},
  2573  			manifestResponse: &apiclient.ManifestResponse{},
  2574  		}, nil)
  2575  
  2576  		ctrl.processAppRefreshQueueItem()
  2577  
  2578  		updatedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(ctrl.namespace).Get(t.Context(), app.Name, metav1.GetOptions{})
  2579  		require.NoError(t, err)
  2580  		assert.Equal(t, test.FakeArgoCDNamespace, updatedApp.Status.ControllerNamespace)
  2581  	})
  2582  	t.Run("set controllerNamespace when the app is in another namespace than the controller", func(t *testing.T) {
  2583  		appNamespace := "app-namespace"
  2584  
  2585  		app := newFakeApp()
  2586  		app.Namespace = appNamespace
  2587  		proj := defaultProj
  2588  		proj.Spec.SourceNamespaces = []string{appNamespace}
  2589  		ctrl := newFakeController(&fakeData{
  2590  			apps:                  []runtime.Object{app, &proj},
  2591  			manifestResponse:      &apiclient.ManifestResponse{},
  2592  			applicationNamespaces: []string{appNamespace},
  2593  		}, nil)
  2594  
  2595  		ctrl.processAppRefreshQueueItem()
  2596  
  2597  		updatedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(appNamespace).Get(t.Context(), app.Name, metav1.GetOptions{})
  2598  		require.NoError(t, err)
  2599  		assert.Equal(t, test.FakeArgoCDNamespace, updatedApp.Status.ControllerNamespace)
  2600  	})
  2601  }
  2602  
  2603  func TestHelmValuesObjectHasReplaceStrategy(t *testing.T) {
  2604  	app := v1alpha1.Application{
  2605  		Status: v1alpha1.ApplicationStatus{Sync: v1alpha1.SyncStatus{ComparedTo: v1alpha1.ComparedTo{
  2606  			Source: v1alpha1.ApplicationSource{
  2607  				Helm: &v1alpha1.ApplicationSourceHelm{
  2608  					ValuesObject: &runtime.RawExtension{
  2609  						Object: &unstructured.Unstructured{Object: map[string]any{"key": []string{"value"}}},
  2610  					},
  2611  				},
  2612  			},
  2613  		}}},
  2614  	}
  2615  
  2616  	appModified := v1alpha1.Application{
  2617  		Status: v1alpha1.ApplicationStatus{Sync: v1alpha1.SyncStatus{ComparedTo: v1alpha1.ComparedTo{
  2618  			Source: v1alpha1.ApplicationSource{
  2619  				Helm: &v1alpha1.ApplicationSourceHelm{
  2620  					ValuesObject: &runtime.RawExtension{
  2621  						Object: &unstructured.Unstructured{Object: map[string]any{"key": []string{"value-modified1"}}},
  2622  					},
  2623  				},
  2624  			},
  2625  		}}},
  2626  	}
  2627  
  2628  	patch, _, err := createMergePatch(
  2629  		app,
  2630  		appModified)
  2631  	require.NoError(t, err)
  2632  	assert.JSONEq(t, `{"status":{"sync":{"comparedTo":{"source":{"helm":{"valuesObject":{"key":["value-modified1"]}}}}}}}`, string(patch))
  2633  }
  2634  
  2635  func TestAppStatusIsReplaced(t *testing.T) {
  2636  	original := &v1alpha1.ApplicationStatus{Sync: v1alpha1.SyncStatus{
  2637  		ComparedTo: v1alpha1.ComparedTo{
  2638  			Destination: v1alpha1.ApplicationDestination{
  2639  				Server: "https://mycluster",
  2640  			},
  2641  		},
  2642  	}}
  2643  
  2644  	updated := &v1alpha1.ApplicationStatus{Sync: v1alpha1.SyncStatus{
  2645  		ComparedTo: v1alpha1.ComparedTo{
  2646  			Destination: v1alpha1.ApplicationDestination{
  2647  				Name: "mycluster",
  2648  			},
  2649  		},
  2650  	}}
  2651  
  2652  	patchData, ok, err := createMergePatch(original, updated)
  2653  
  2654  	require.NoError(t, err)
  2655  	require.True(t, ok)
  2656  	patchObj := map[string]any{}
  2657  	require.NoError(t, json.Unmarshal(patchData, &patchObj))
  2658  
  2659  	val, has, err := unstructured.NestedFieldNoCopy(patchObj, "sync", "comparedTo", "destination", "server")
  2660  	require.NoError(t, err)
  2661  	require.True(t, has)
  2662  	require.Nil(t, val)
  2663  }
  2664  
  2665  func TestAlreadyAttemptSync(t *testing.T) {
  2666  	app := newFakeApp()
  2667  	defaultRevision := app.Status.OperationState.SyncResult.Revision
  2668  
  2669  	t.Run("no operation state", func(t *testing.T) {
  2670  		app := app.DeepCopy()
  2671  		app.Status.OperationState = nil
  2672  		attempted, _, _ := alreadyAttemptedSync(app, []string{defaultRevision}, true)
  2673  		assert.False(t, attempted)
  2674  	})
  2675  
  2676  	t.Run("no sync result for running sync", func(t *testing.T) {
  2677  		app := app.DeepCopy()
  2678  		app.Status.OperationState.SyncResult = nil
  2679  		app.Status.OperationState.Phase = synccommon.OperationRunning
  2680  		attempted, _, _ := alreadyAttemptedSync(app, []string{defaultRevision}, true)
  2681  		assert.False(t, attempted)
  2682  	})
  2683  
  2684  	t.Run("no sync result for completed sync", func(t *testing.T) {
  2685  		app := app.DeepCopy()
  2686  		app.Status.OperationState.SyncResult = nil
  2687  		app.Status.OperationState.Phase = synccommon.OperationError
  2688  		attempted, _, _ := alreadyAttemptedSync(app, []string{defaultRevision}, true)
  2689  		assert.True(t, attempted)
  2690  	})
  2691  
  2692  	t.Run("single source", func(t *testing.T) {
  2693  		t.Run("no revision", func(t *testing.T) {
  2694  			attempted, _, _ := alreadyAttemptedSync(app, []string{}, true)
  2695  			assert.False(t, attempted)
  2696  		})
  2697  
  2698  		t.Run("empty revision", func(t *testing.T) {
  2699  			attempted, _, _ := alreadyAttemptedSync(app, []string{""}, true)
  2700  			assert.False(t, attempted)
  2701  		})
  2702  
  2703  		t.Run("too many revision", func(t *testing.T) {
  2704  			app := app.DeepCopy()
  2705  			app.Status.OperationState.SyncResult.Revision = "sha"
  2706  			attempted, _, _ := alreadyAttemptedSync(app, []string{"sha", "sha2"}, true)
  2707  			assert.False(t, attempted)
  2708  		})
  2709  
  2710  		t.Run("same manifest, same SHA with changes", func(t *testing.T) {
  2711  			app := app.DeepCopy()
  2712  			app.Status.OperationState.SyncResult.Revision = "sha"
  2713  			attempted, _, _ := alreadyAttemptedSync(app, []string{"sha"}, true)
  2714  			assert.True(t, attempted)
  2715  		})
  2716  
  2717  		t.Run("same manifest, different SHA with changes", func(t *testing.T) {
  2718  			app := app.DeepCopy()
  2719  			app.Status.OperationState.SyncResult.Revision = "sha1"
  2720  			attempted, _, _ := alreadyAttemptedSync(app, []string{"sha2"}, true)
  2721  			assert.False(t, attempted)
  2722  		})
  2723  
  2724  		t.Run("same manifest, different SHA without changes", func(t *testing.T) {
  2725  			app := app.DeepCopy()
  2726  			app.Status.OperationState.SyncResult.Revision = "sha1"
  2727  			attempted, _, _ := alreadyAttemptedSync(app, []string{"sha2"}, false)
  2728  			assert.True(t, attempted)
  2729  		})
  2730  
  2731  		t.Run("different manifest, same SHA with changes", func(t *testing.T) {
  2732  			// This test represents the case where the user changed a source's target revision to a new branch, but it
  2733  			// points to the same revision as the old branch. We currently do not consider this as having been "already
  2734  			// attempted." In the future we may want to short-circuit the auto-sync in these cases.
  2735  			app := app.DeepCopy()
  2736  			app.Status.OperationState.SyncResult.Source = v1alpha1.ApplicationSource{TargetRevision: "branch1"}
  2737  			app.Spec.Source = &v1alpha1.ApplicationSource{TargetRevision: "branch2"}
  2738  			app.Status.OperationState.SyncResult.Revision = "sha"
  2739  			attempted, _, _ := alreadyAttemptedSync(app, []string{"sha"}, true)
  2740  			assert.False(t, attempted)
  2741  		})
  2742  
  2743  		t.Run("different manifest, different SHA with changes", func(t *testing.T) {
  2744  			app := app.DeepCopy()
  2745  			app.Status.OperationState.SyncResult.Source = v1alpha1.ApplicationSource{Path: "folder1"}
  2746  			app.Spec.Source = &v1alpha1.ApplicationSource{Path: "folder2"}
  2747  			app.Status.OperationState.SyncResult.Revision = "sha1"
  2748  			attempted, _, _ := alreadyAttemptedSync(app, []string{"sha2"}, true)
  2749  			assert.False(t, attempted)
  2750  		})
  2751  
  2752  		t.Run("different manifest, different SHA without changes", func(t *testing.T) {
  2753  			app := app.DeepCopy()
  2754  			app.Status.OperationState.SyncResult.Source = v1alpha1.ApplicationSource{Path: "folder1"}
  2755  			app.Spec.Source = &v1alpha1.ApplicationSource{Path: "folder2"}
  2756  			app.Status.OperationState.SyncResult.Revision = "sha1"
  2757  			attempted, _, _ := alreadyAttemptedSync(app, []string{"sha2"}, false)
  2758  			assert.False(t, attempted)
  2759  		})
  2760  
  2761  		t.Run("different manifest, same SHA without changes", func(t *testing.T) {
  2762  			app := app.DeepCopy()
  2763  			app.Status.OperationState.SyncResult.Source = v1alpha1.ApplicationSource{Path: "folder1"}
  2764  			app.Spec.Source = &v1alpha1.ApplicationSource{Path: "folder2"}
  2765  			app.Status.OperationState.SyncResult.Revision = "sha"
  2766  			attempted, _, _ := alreadyAttemptedSync(app, []string{"sha"}, false)
  2767  			assert.False(t, attempted)
  2768  		})
  2769  	})
  2770  
  2771  	t.Run("multi-source", func(t *testing.T) {
  2772  		app := app.DeepCopy()
  2773  		app.Status.OperationState.SyncResult.Sources = []v1alpha1.ApplicationSource{{Path: "folder1"}, {Path: "folder2"}}
  2774  		app.Spec.Sources = []v1alpha1.ApplicationSource{{Path: "folder1"}, {Path: "folder2"}}
  2775  
  2776  		t.Run("same manifest, same SHAs with changes", func(t *testing.T) {
  2777  			app := app.DeepCopy()
  2778  			app.Status.OperationState.SyncResult.Revisions = []string{"sha_a", "sha_b"}
  2779  			attempted, _, _ := alreadyAttemptedSync(app, []string{"sha_a", "sha_b"}, true)
  2780  			assert.True(t, attempted)
  2781  		})
  2782  
  2783  		t.Run("same manifest, different SHAs with changes", func(t *testing.T) {
  2784  			app := app.DeepCopy()
  2785  			app.Status.OperationState.SyncResult.Revisions = []string{"sha_a_=", "sha_b_1"}
  2786  			attempted, _, _ := alreadyAttemptedSync(app, []string{"sha_a_2", "sha_b_2"}, true)
  2787  			assert.False(t, attempted)
  2788  		})
  2789  
  2790  		t.Run("same manifest, different SHA without changes", func(t *testing.T) {
  2791  			app := app.DeepCopy()
  2792  			app.Status.OperationState.SyncResult.Revisions = []string{"sha_a_=", "sha_b_1"}
  2793  			attempted, _, _ := alreadyAttemptedSync(app, []string{"sha_a_2", "sha_b_2"}, false)
  2794  			assert.True(t, attempted)
  2795  		})
  2796  
  2797  		t.Run("different manifest, same SHA with changes", func(t *testing.T) {
  2798  			// This test represents the case where the user changed a source's target revision to a new branch, but it
  2799  			// points to the same revision as the old branch. We currently do not consider this as having been "already
  2800  			// attempted." In the future we may want to short-circuit the auto-sync in these cases.
  2801  			app := app.DeepCopy()
  2802  			app.Status.OperationState.SyncResult.Sources = []v1alpha1.ApplicationSource{{TargetRevision: "branch1"}, {TargetRevision: "branch2"}}
  2803  			app.Spec.Sources = []v1alpha1.ApplicationSource{{TargetRevision: "branch1"}, {TargetRevision: "branch3"}}
  2804  			app.Status.OperationState.SyncResult.Revisions = []string{"sha_a_2", "sha_b_2"}
  2805  			attempted, _, _ := alreadyAttemptedSync(app, []string{"sha_a_2", "sha_b_2"}, false)
  2806  			assert.False(t, attempted)
  2807  		})
  2808  
  2809  		t.Run("different manifest, different SHA with changes", func(t *testing.T) {
  2810  			app := app.DeepCopy()
  2811  			app.Status.OperationState.SyncResult.Sources = []v1alpha1.ApplicationSource{{Path: "folder1"}, {Path: "folder2"}}
  2812  			app.Spec.Sources = []v1alpha1.ApplicationSource{{Path: "folder1"}, {Path: "folder3"}}
  2813  			app.Status.OperationState.SyncResult.Revisions = []string{"sha_a", "sha_b"}
  2814  			attempted, _, _ := alreadyAttemptedSync(app, []string{"sha_a", "sha_b_2"}, true)
  2815  			assert.False(t, attempted)
  2816  		})
  2817  
  2818  		t.Run("different manifest, different SHA without changes", func(t *testing.T) {
  2819  			app := app.DeepCopy()
  2820  			app.Status.OperationState.SyncResult.Sources = []v1alpha1.ApplicationSource{{Path: "folder1"}, {Path: "folder2"}}
  2821  			app.Spec.Sources = []v1alpha1.ApplicationSource{{Path: "folder1"}, {Path: "folder3"}}
  2822  			app.Status.OperationState.SyncResult.Revisions = []string{"sha_a", "sha_b"}
  2823  			attempted, _, _ := alreadyAttemptedSync(app, []string{"sha_a", "sha_b_2"}, false)
  2824  			assert.False(t, attempted)
  2825  		})
  2826  
  2827  		t.Run("different manifest, same SHA without changes", func(t *testing.T) {
  2828  			app := app.DeepCopy()
  2829  			app.Status.OperationState.SyncResult.Sources = []v1alpha1.ApplicationSource{{Path: "folder1"}, {Path: "folder2"}}
  2830  			app.Spec.Sources = []v1alpha1.ApplicationSource{{Path: "folder1"}, {Path: "folder3"}}
  2831  			app.Status.OperationState.SyncResult.Revisions = []string{"sha_a", "sha_b"}
  2832  			attempted, _, _ := alreadyAttemptedSync(app, []string{"sha_a", "sha_b"}, false)
  2833  			assert.False(t, attempted)
  2834  		})
  2835  	})
  2836  }
  2837  
  2838  func assertDurationAround(t *testing.T, expected time.Duration, actual time.Duration) {
  2839  	t.Helper()
  2840  	delta := time.Second / 2
  2841  	assert.GreaterOrEqual(t, expected, actual-delta)
  2842  	assert.LessOrEqual(t, expected, actual+delta)
  2843  }
  2844  
  2845  func TestSelfHealRemainingBackoff(t *testing.T) {
  2846  	ctrl := newFakeController(&fakeData{}, nil)
  2847  	ctrl.selfHealBackoff = &wait.Backoff{
  2848  		Factor:   3,
  2849  		Duration: 2 * time.Second,
  2850  		Cap:      2 * time.Minute,
  2851  	}
  2852  	app := &v1alpha1.Application{
  2853  		Status: v1alpha1.ApplicationStatus{
  2854  			OperationState: &v1alpha1.OperationState{
  2855  				Operation: v1alpha1.Operation{
  2856  					Sync: &v1alpha1.SyncOperation{},
  2857  				},
  2858  			},
  2859  		},
  2860  	}
  2861  
  2862  	testCases := []struct {
  2863  		attempts         int
  2864  		finishedAt       *metav1.Time
  2865  		expectedDuration time.Duration
  2866  		shouldSelfHeal   bool
  2867  	}{{
  2868  		attempts:         0,
  2869  		finishedAt:       ptr.To(metav1.Now()),
  2870  		expectedDuration: 0,
  2871  		shouldSelfHeal:   true,
  2872  	}, {
  2873  		attempts:         1,
  2874  		finishedAt:       ptr.To(metav1.Now()),
  2875  		expectedDuration: 2 * time.Second,
  2876  		shouldSelfHeal:   false,
  2877  	}, {
  2878  		attempts:         2,
  2879  		finishedAt:       ptr.To(metav1.Now()),
  2880  		expectedDuration: 6 * time.Second,
  2881  		shouldSelfHeal:   false,
  2882  	}, {
  2883  		attempts:         3,
  2884  		finishedAt:       nil,
  2885  		expectedDuration: 18 * time.Second,
  2886  		shouldSelfHeal:   false,
  2887  	}, {
  2888  		attempts:         4,
  2889  		finishedAt:       nil,
  2890  		expectedDuration: 54 * time.Second,
  2891  		shouldSelfHeal:   false,
  2892  	}, {
  2893  		attempts:         5,
  2894  		finishedAt:       nil,
  2895  		expectedDuration: 120 * time.Second,
  2896  		shouldSelfHeal:   false,
  2897  	}, {
  2898  		attempts:         6,
  2899  		finishedAt:       nil,
  2900  		expectedDuration: 120 * time.Second,
  2901  		shouldSelfHeal:   false,
  2902  	}, {
  2903  		attempts:         6,
  2904  		finishedAt:       ptr.To(metav1.Now()),
  2905  		expectedDuration: 120 * time.Second,
  2906  		shouldSelfHeal:   false,
  2907  	}, {
  2908  		attempts:         40,
  2909  		finishedAt:       &metav1.Time{Time: time.Now().Add(-1 * time.Minute)},
  2910  		expectedDuration: 60 * time.Second,
  2911  		shouldSelfHeal:   false,
  2912  	}}
  2913  
  2914  	for i := range testCases {
  2915  		tc := testCases[i]
  2916  		t.Run(fmt.Sprintf("test case %d", i), func(t *testing.T) {
  2917  			app.Status.OperationState.FinishedAt = tc.finishedAt
  2918  			duration := ctrl.selfHealRemainingBackoff(app, tc.attempts)
  2919  			shouldSelfHeal := duration <= 0
  2920  			require.Equal(t, tc.shouldSelfHeal, shouldSelfHeal)
  2921  			assertDurationAround(t, tc.expectedDuration, duration)
  2922  		})
  2923  	}
  2924  }
  2925  
  2926  func TestSelfHealBackoffCooldownElapsed(t *testing.T) {
  2927  	cooldown := time.Second * 30
  2928  	ctrl := newFakeController(&fakeData{}, nil)
  2929  	ctrl.selfHealBackoffCooldown = cooldown
  2930  
  2931  	app := &v1alpha1.Application{
  2932  		Status: v1alpha1.ApplicationStatus{
  2933  			OperationState: &v1alpha1.OperationState{
  2934  				Phase: synccommon.OperationSucceeded,
  2935  			},
  2936  		},
  2937  	}
  2938  
  2939  	t.Run("operation not completed", func(t *testing.T) {
  2940  		app := app.DeepCopy()
  2941  		app.Status.OperationState.FinishedAt = nil
  2942  		elapsed := ctrl.selfHealBackoffCooldownElapsed(app)
  2943  		assert.True(t, elapsed)
  2944  	})
  2945  
  2946  	t.Run("successful operation finised after cooldown", func(t *testing.T) {
  2947  		app := app.DeepCopy()
  2948  		app.Status.OperationState.FinishedAt = &metav1.Time{Time: time.Now().Add(-cooldown)}
  2949  		elapsed := ctrl.selfHealBackoffCooldownElapsed(app)
  2950  		assert.True(t, elapsed)
  2951  	})
  2952  
  2953  	t.Run("unsuccessful operation finised after cooldown", func(t *testing.T) {
  2954  		app := app.DeepCopy()
  2955  		app.Status.OperationState.Phase = synccommon.OperationFailed
  2956  		app.Status.OperationState.FinishedAt = &metav1.Time{Time: time.Now().Add(-cooldown)}
  2957  		elapsed := ctrl.selfHealBackoffCooldownElapsed(app)
  2958  		assert.False(t, elapsed)
  2959  	})
  2960  
  2961  	t.Run("successful operation finised before cooldown", func(t *testing.T) {
  2962  		app := app.DeepCopy()
  2963  		app.Status.OperationState.FinishedAt = &metav1.Time{Time: time.Now()}
  2964  		elapsed := ctrl.selfHealBackoffCooldownElapsed(app)
  2965  		assert.False(t, elapsed)
  2966  	})
  2967  }