github.com/argoproj/argo-cd@v1.8.7/server/application/application_test.go (about)

     1  package application
     2  
     3  import (
     4  	"context"
     5  	coreerrors "errors"
     6  	"testing"
     7  	"time"
     8  
     9  	synccommon "github.com/argoproj/gitops-engine/pkg/sync/common"
    10  	"github.com/argoproj/gitops-engine/pkg/utils/kube/kubetest"
    11  	"github.com/argoproj/pkg/sync"
    12  	"github.com/dgrijalva/jwt-go/v4"
    13  	"github.com/ghodss/yaml"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/mock"
    16  	"google.golang.org/grpc/codes"
    17  	"google.golang.org/grpc/status"
    18  	v1 "k8s.io/api/core/v1"
    19  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    20  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    21  	"k8s.io/apimachinery/pkg/runtime"
    22  	"k8s.io/apimachinery/pkg/watch"
    23  	"k8s.io/client-go/kubernetes/fake"
    24  	kubetesting "k8s.io/client-go/testing"
    25  	k8scache "k8s.io/client-go/tools/cache"
    26  	"k8s.io/utils/pointer"
    27  
    28  	"github.com/argoproj/argo-cd/common"
    29  	"github.com/argoproj/argo-cd/pkg/apiclient/application"
    30  	appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
    31  	apps "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
    32  	appinformer "github.com/argoproj/argo-cd/pkg/client/informers/externalversions"
    33  	"github.com/argoproj/argo-cd/reposerver/apiclient"
    34  	"github.com/argoproj/argo-cd/reposerver/apiclient/mocks"
    35  	"github.com/argoproj/argo-cd/server/rbacpolicy"
    36  	"github.com/argoproj/argo-cd/test"
    37  	"github.com/argoproj/argo-cd/util/assets"
    38  	"github.com/argoproj/argo-cd/util/cache"
    39  	"github.com/argoproj/argo-cd/util/db"
    40  	"github.com/argoproj/argo-cd/util/errors"
    41  	"github.com/argoproj/argo-cd/util/rbac"
    42  	"github.com/argoproj/argo-cd/util/settings"
    43  )
    44  
    45  const (
    46  	testNamespace = "default"
    47  	fakeRepoURL   = "https://git.com/repo.git"
    48  )
    49  
    50  func fakeRepo() *appsv1.Repository {
    51  	return &appsv1.Repository{
    52  		Repo: fakeRepoURL,
    53  	}
    54  }
    55  
    56  func fakeCluster() *appsv1.Cluster {
    57  	return &appsv1.Cluster{
    58  		Server: "https://cluster-api.com",
    59  		Name:   "fake-cluster",
    60  		Config: appsv1.ClusterConfig{},
    61  	}
    62  }
    63  
    64  func fakeAppList() *apiclient.AppList {
    65  	return &apiclient.AppList{
    66  		Apps: map[string]string{
    67  			"some/path": "Ksonnet",
    68  		},
    69  	}
    70  }
    71  
    72  // return an ApplicationServiceServer which returns fake data
    73  func newTestAppServer(objects ...runtime.Object) *Server {
    74  	kubeclientset := fake.NewSimpleClientset(&v1.ConfigMap{
    75  		ObjectMeta: metav1.ObjectMeta{
    76  			Namespace: testNamespace,
    77  			Name:      "argocd-cm",
    78  			Labels: map[string]string{
    79  				"app.kubernetes.io/part-of": "argocd",
    80  			},
    81  		},
    82  	}, &v1.Secret{
    83  		ObjectMeta: metav1.ObjectMeta{
    84  			Name:      "argocd-secret",
    85  			Namespace: testNamespace,
    86  		},
    87  		Data: map[string][]byte{
    88  			"admin.password":   []byte("test"),
    89  			"server.secretkey": []byte("test"),
    90  		},
    91  	})
    92  	ctx := context.Background()
    93  	db := db.NewDB(testNamespace, settings.NewSettingsManager(ctx, kubeclientset, testNamespace), kubeclientset)
    94  	_, err := db.CreateRepository(ctx, fakeRepo())
    95  	errors.CheckError(err)
    96  	_, err = db.CreateCluster(ctx, fakeCluster())
    97  	errors.CheckError(err)
    98  
    99  	mockRepoServiceClient := mocks.RepoServerServiceClient{}
   100  	mockRepoServiceClient.On("ListApps", mock.Anything, mock.Anything).Return(fakeAppList(), nil)
   101  	mockRepoServiceClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(&apiclient.ManifestResponse{}, nil)
   102  	mockRepoServiceClient.On("GetAppDetails", mock.Anything, mock.Anything).Return(&apiclient.RepoAppDetailsResponse{}, nil)
   103  
   104  	mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mockRepoServiceClient}
   105  
   106  	defaultProj := &appsv1.AppProject{
   107  		ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "default"},
   108  		Spec: appsv1.AppProjectSpec{
   109  			SourceRepos:  []string{"*"},
   110  			Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
   111  		},
   112  	}
   113  	myProj := &appsv1.AppProject{
   114  		ObjectMeta: metav1.ObjectMeta{Name: "my-proj", Namespace: "default"},
   115  		Spec: appsv1.AppProjectSpec{
   116  			SourceRepos:  []string{"*"},
   117  			Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
   118  		},
   119  	}
   120  	projWithSyncWindows := &appsv1.AppProject{
   121  		ObjectMeta: metav1.ObjectMeta{Name: "proj-maint", Namespace: "default"},
   122  		Spec: appsv1.AppProjectSpec{
   123  			SourceRepos:  []string{"*"},
   124  			Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
   125  			SyncWindows:  appsv1.SyncWindows{},
   126  		},
   127  	}
   128  	matchingWindow := &appsv1.SyncWindow{
   129  		Kind:         "allow",
   130  		Schedule:     "* * * * *",
   131  		Duration:     "1h",
   132  		Applications: []string{"test-app"},
   133  	}
   134  	projWithSyncWindows.Spec.SyncWindows = append(projWithSyncWindows.Spec.SyncWindows, matchingWindow)
   135  
   136  	objects = append(objects, defaultProj, myProj, projWithSyncWindows)
   137  
   138  	fakeAppsClientset := apps.NewSimpleClientset(objects...)
   139  	factory := appinformer.NewFilteredSharedInformerFactory(fakeAppsClientset, 0, "", func(options *metav1.ListOptions) {})
   140  	fakeProjLister := factory.Argoproj().V1alpha1().AppProjects().Lister().AppProjects(testNamespace)
   141  
   142  	enforcer := rbac.NewEnforcer(kubeclientset, testNamespace, common.ArgoCDRBACConfigMapName, nil)
   143  	_ = enforcer.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
   144  	enforcer.SetDefaultRole("role:admin")
   145  	enforcer.SetClaimsEnforcerFunc(rbacpolicy.NewRBACPolicyEnforcer(enforcer, fakeProjLister).EnforceClaims)
   146  
   147  	settingsMgr := settings.NewSettingsManager(ctx, kubeclientset, testNamespace)
   148  
   149  	// populate the app informer with the fake objects
   150  	appInformer := factory.Argoproj().V1alpha1().Applications().Informer()
   151  	// TODO(jessesuen): probably should return cancel function so tests can stop background informer
   152  	//ctx, cancel := context.WithCancel(context.Background())
   153  	go appInformer.Run(ctx.Done())
   154  	if !k8scache.WaitForCacheSync(ctx.Done(), appInformer.HasSynced) {
   155  		panic("Timed out waiting for caches to sync")
   156  	}
   157  
   158  	projInformer := factory.Argoproj().V1alpha1().AppProjects().Informer()
   159  	go projInformer.Run(ctx.Done())
   160  	if !k8scache.WaitForCacheSync(ctx.Done(), projInformer.HasSynced) {
   161  		panic("Timed out waiting for caches to sync")
   162  	}
   163  
   164  	server := NewServer(
   165  		testNamespace,
   166  		kubeclientset,
   167  		fakeAppsClientset,
   168  		factory.Argoproj().V1alpha1().Applications().Lister().Applications(testNamespace),
   169  		appInformer,
   170  		mockRepoClient,
   171  		nil,
   172  		&kubetest.MockKubectlCmd{},
   173  		db,
   174  		enforcer,
   175  		sync.NewKeyLock(),
   176  		settingsMgr,
   177  		projInformer,
   178  	)
   179  	return server.(*Server)
   180  }
   181  
   182  const fakeApp = `
   183  apiVersion: argoproj.io/v1alpha1
   184  kind: Application
   185  metadata:
   186    name: test-app
   187    namespace: default
   188  spec:
   189    source:
   190      path: some/path
   191      repoURL: https://github.com/argoproj/argocd-example-apps.git
   192      targetRevision: HEAD
   193      ksonnet:
   194        environment: default
   195    destination:
   196      namespace: ` + test.FakeDestNamespace + `
   197      server: https://cluster-api.com
   198  `
   199  
   200  const fakeAppWithDestName = `
   201  apiVersion: argoproj.io/v1alpha1
   202  kind: Application
   203  metadata:
   204    name: test-app
   205    namespace: default
   206  spec:
   207    source:
   208      path: some/path
   209      repoURL: https://github.com/argoproj/argocd-example-apps.git
   210      targetRevision: HEAD
   211      ksonnet:
   212        environment: default
   213    destination:
   214      namespace: ` + test.FakeDestNamespace + `
   215      name: fake-cluster
   216  `
   217  
   218  const fakeAppWithAnnotations = `
   219  apiVersion: argoproj.io/v1alpha1
   220  kind: Application
   221  metadata:
   222    name: test-app
   223    namespace: default
   224    annotations:
   225      test.annotation: test
   226  spec:
   227    source:
   228      path: some/path
   229      repoURL: https://github.com/argoproj/argocd-example-apps.git
   230      targetRevision: HEAD
   231      ksonnet:
   232        environment: default
   233    destination:
   234      namespace: ` + test.FakeDestNamespace + `
   235      server: https://cluster-api.com
   236  `
   237  
   238  func newTestAppWithDestName(opts ...func(app *appsv1.Application)) *appsv1.Application {
   239  	return createTestApp(fakeAppWithDestName, opts...)
   240  }
   241  
   242  func newTestApp(opts ...func(app *appsv1.Application)) *appsv1.Application {
   243  	return createTestApp(fakeApp, opts...)
   244  }
   245  
   246  func newTestAppWithAnnotations(opts ...func(app *appsv1.Application)) *appsv1.Application {
   247  	return createTestApp(fakeAppWithAnnotations, opts...)
   248  }
   249  
   250  func createTestApp(testApp string, opts ...func(app *appsv1.Application)) *appsv1.Application {
   251  	var app appsv1.Application
   252  	err := yaml.Unmarshal([]byte(testApp), &app)
   253  	if err != nil {
   254  		panic(err)
   255  	}
   256  	for i := range opts {
   257  		opts[i](&app)
   258  	}
   259  	return &app
   260  }
   261  
   262  func TestListApps(t *testing.T) {
   263  	appServer := newTestAppServer(newTestApp(func(app *appsv1.Application) {
   264  		app.Name = "bcd"
   265  	}), newTestApp(func(app *appsv1.Application) {
   266  		app.Name = "abc"
   267  	}), newTestApp(func(app *appsv1.Application) {
   268  		app.Name = "def"
   269  	}))
   270  
   271  	res, err := appServer.List(context.Background(), &application.ApplicationQuery{})
   272  	assert.NoError(t, err)
   273  	var names []string
   274  	for i := range res.Items {
   275  		names = append(names, res.Items[i].Name)
   276  	}
   277  	assert.Equal(t, []string{"abc", "bcd", "def"}, names)
   278  }
   279  
   280  func TestCreateApp(t *testing.T) {
   281  	testApp := newTestApp()
   282  	appServer := newTestAppServer()
   283  	testApp.Spec.Project = ""
   284  	createReq := application.ApplicationCreateRequest{
   285  		Application: *testApp,
   286  	}
   287  	app, err := appServer.Create(context.Background(), &createReq)
   288  	assert.NoError(t, err)
   289  	assert.NotNil(t, app)
   290  	assert.NotNil(t, app.Spec)
   291  	assert.Equal(t, app.Spec.Project, "default")
   292  }
   293  
   294  func TestCreateAppWithDestName(t *testing.T) {
   295  	appServer := newTestAppServer()
   296  	testApp := newTestAppWithDestName()
   297  	createReq := application.ApplicationCreateRequest{
   298  		Application: *testApp,
   299  	}
   300  	app, err := appServer.Create(context.Background(), &createReq)
   301  	assert.NoError(t, err)
   302  	assert.NotNil(t, app)
   303  	assert.Equal(t, app.Spec.Destination.Server, "https://cluster-api.com")
   304  }
   305  
   306  func TestUpdateApp(t *testing.T) {
   307  	testApp := newTestApp()
   308  	appServer := newTestAppServer(testApp)
   309  	testApp.Spec.Project = ""
   310  	app, err := appServer.Update(context.Background(), &application.ApplicationUpdateRequest{
   311  		Application: testApp,
   312  	})
   313  	assert.Nil(t, err)
   314  	assert.Equal(t, app.Spec.Project, "default")
   315  }
   316  
   317  func TestUpdateAppSpec(t *testing.T) {
   318  	testApp := newTestApp()
   319  	appServer := newTestAppServer(testApp)
   320  	testApp.Spec.Project = ""
   321  	spec, err := appServer.UpdateSpec(context.Background(), &application.ApplicationUpdateSpecRequest{
   322  		Name: &testApp.Name,
   323  		Spec: testApp.Spec,
   324  	})
   325  	assert.NoError(t, err)
   326  	assert.Equal(t, "default", spec.Project)
   327  	app, err := appServer.Get(context.Background(), &application.ApplicationQuery{Name: &testApp.Name})
   328  	assert.NoError(t, err)
   329  	assert.Equal(t, "default", app.Spec.Project)
   330  }
   331  
   332  func TestDeleteApp(t *testing.T) {
   333  	ctx := context.Background()
   334  	appServer := newTestAppServer()
   335  	createReq := application.ApplicationCreateRequest{
   336  		Application: *newTestApp(),
   337  	}
   338  	app, err := appServer.Create(ctx, &createReq)
   339  	assert.Nil(t, err)
   340  
   341  	app, err = appServer.Get(ctx, &application.ApplicationQuery{Name: &app.Name})
   342  	assert.Nil(t, err)
   343  	assert.NotNil(t, app)
   344  
   345  	fakeAppCs := appServer.appclientset.(*apps.Clientset)
   346  	// this removes the default */* reactor so we can set our own patch/delete reactor
   347  	fakeAppCs.ReactionChain = nil
   348  	patched := false
   349  	deleted := false
   350  	fakeAppCs.AddReactor("patch", "applications", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
   351  		patched = true
   352  		return true, nil, nil
   353  	})
   354  	fakeAppCs.AddReactor("delete", "applications", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
   355  		deleted = true
   356  		return true, nil, nil
   357  	})
   358  	appServer.appclientset = fakeAppCs
   359  
   360  	trueVar := true
   361  	_, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, Cascade: &trueVar})
   362  	assert.Nil(t, err)
   363  	assert.True(t, patched)
   364  	assert.True(t, deleted)
   365  
   366  	// now call delete with cascade=false. patch should not be called
   367  	falseVar := false
   368  	patched = false
   369  	deleted = false
   370  	_, err = appServer.Delete(ctx, &application.ApplicationDeleteRequest{Name: &app.Name, Cascade: &falseVar})
   371  	assert.Nil(t, err)
   372  	assert.False(t, patched)
   373  	assert.True(t, deleted)
   374  }
   375  
   376  func TestDeleteApp_InvalidName(t *testing.T) {
   377  	appServer := newTestAppServer()
   378  	_, err := appServer.Delete(context.Background(), &application.ApplicationDeleteRequest{
   379  		Name: pointer.StringPtr("foo"),
   380  	})
   381  	if !assert.Error(t, err) {
   382  		return
   383  	}
   384  	assert.True(t, apierrors.IsNotFound(err))
   385  }
   386  
   387  func TestSyncAndTerminate(t *testing.T) {
   388  	ctx := context.Background()
   389  	appServer := newTestAppServer()
   390  	testApp := newTestApp()
   391  	testApp.Spec.Source.RepoURL = "https://github.com/argoproj/argo-cd.git"
   392  	createReq := application.ApplicationCreateRequest{
   393  		Application: *testApp,
   394  	}
   395  	app, err := appServer.Create(ctx, &createReq)
   396  	assert.Nil(t, err)
   397  
   398  	app, err = appServer.Sync(ctx, &application.ApplicationSyncRequest{Name: &app.Name})
   399  	assert.Nil(t, err)
   400  	assert.NotNil(t, app)
   401  	assert.NotNil(t, app.Operation)
   402  
   403  	events, err := appServer.kubeclientset.CoreV1().Events(appServer.ns).List(context.Background(), metav1.ListOptions{})
   404  	assert.Nil(t, err)
   405  	event := events.Items[1]
   406  
   407  	assert.Regexp(t, ".*initiated sync to HEAD \\([0-9A-Fa-f]{40}\\).*", event.Message)
   408  
   409  	// set status.operationState to pretend that an operation has started by controller
   410  	app.Status.OperationState = &appsv1.OperationState{
   411  		Operation: *app.Operation,
   412  		Phase:     synccommon.OperationRunning,
   413  		StartedAt: metav1.NewTime(time.Now()),
   414  	}
   415  	_, err = appServer.appclientset.ArgoprojV1alpha1().Applications(appServer.ns).Update(context.Background(), app, metav1.UpdateOptions{})
   416  	assert.Nil(t, err)
   417  
   418  	resp, err := appServer.TerminateOperation(ctx, &application.OperationTerminateRequest{Name: &app.Name})
   419  	assert.Nil(t, err)
   420  	assert.NotNil(t, resp)
   421  
   422  	app, err = appServer.Get(ctx, &application.ApplicationQuery{Name: &app.Name})
   423  	assert.Nil(t, err)
   424  	assert.NotNil(t, app)
   425  	assert.Equal(t, synccommon.OperationTerminating, app.Status.OperationState.Phase)
   426  }
   427  
   428  func TestSyncHelm(t *testing.T) {
   429  	ctx := context.Background()
   430  	appServer := newTestAppServer()
   431  	testApp := newTestApp()
   432  	testApp.Spec.Source.RepoURL = "https://argoproj.github.io/argo-helm"
   433  	testApp.Spec.Source.Path = ""
   434  	testApp.Spec.Source.Chart = "argo-cd"
   435  	testApp.Spec.Source.TargetRevision = "0.7.*"
   436  
   437  	app, err := appServer.Create(ctx, &application.ApplicationCreateRequest{Application: *testApp})
   438  	assert.NoError(t, err)
   439  
   440  	app, err = appServer.Sync(ctx, &application.ApplicationSyncRequest{Name: &app.Name})
   441  	assert.NoError(t, err)
   442  	assert.NotNil(t, app)
   443  	assert.NotNil(t, app.Operation)
   444  
   445  	events, err := appServer.kubeclientset.CoreV1().Events(appServer.ns).List(context.Background(), metav1.ListOptions{})
   446  	assert.NoError(t, err)
   447  	assert.Equal(t, "Unknown user initiated sync to 0.7.* (0.7.2)", events.Items[1].Message)
   448  }
   449  
   450  func TestRollbackApp(t *testing.T) {
   451  	testApp := newTestApp()
   452  	testApp.Status.History = []appsv1.RevisionHistory{{
   453  		ID:       1,
   454  		Revision: "abc",
   455  		Source:   *testApp.Spec.Source.DeepCopy(),
   456  	}}
   457  	appServer := newTestAppServer(testApp)
   458  
   459  	updatedApp, err := appServer.Rollback(context.Background(), &application.ApplicationRollbackRequest{
   460  		Name: &testApp.Name,
   461  		ID:   1,
   462  	})
   463  
   464  	assert.Nil(t, err)
   465  
   466  	assert.NotNil(t, updatedApp.Operation)
   467  	assert.NotNil(t, updatedApp.Operation.Sync)
   468  	assert.NotNil(t, updatedApp.Operation.Sync.Source)
   469  	assert.Equal(t, "abc", updatedApp.Operation.Sync.Revision)
   470  }
   471  
   472  func TestUpdateAppProject(t *testing.T) {
   473  	testApp := newTestApp()
   474  	ctx := context.Background()
   475  	// nolint:staticcheck
   476  	ctx = context.WithValue(ctx, "claims", &jwt.StandardClaims{Subject: "admin"})
   477  	appServer := newTestAppServer(testApp)
   478  	appServer.enf.SetDefaultRole("")
   479  
   480  	// Verify normal update works (without changing project)
   481  	_ = appServer.enf.SetBuiltinPolicy(`p, admin, applications, update, default/test-app, allow`)
   482  	_, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
   483  	assert.NoError(t, err)
   484  
   485  	// Verify caller cannot update to another project
   486  	testApp.Spec.Project = "my-proj"
   487  	_, err = appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
   488  	assert.Equal(t, status.Code(err), codes.PermissionDenied)
   489  
   490  	// Verify inability to change projects without create privileges in new project
   491  	_ = appServer.enf.SetBuiltinPolicy(`
   492  p, admin, applications, update, default/test-app, allow
   493  p, admin, applications, update, my-proj/test-app, allow
   494  `)
   495  	_, err = appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
   496  	assert.Equal(t, status.Code(err), codes.PermissionDenied)
   497  
   498  	// Verify inability to change projects without update privileges in new project
   499  	_ = appServer.enf.SetBuiltinPolicy(`
   500  p, admin, applications, update, default/test-app, allow
   501  p, admin, applications, create, my-proj/test-app, allow
   502  `)
   503  	_, err = appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
   504  	assert.Equal(t, status.Code(err), codes.PermissionDenied)
   505  
   506  	// Verify inability to change projects without update privileges in old project
   507  	_ = appServer.enf.SetBuiltinPolicy(`
   508  p, admin, applications, create, my-proj/test-app, allow
   509  p, admin, applications, update, my-proj/test-app, allow
   510  `)
   511  	_, err = appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
   512  	assert.Equal(t, status.Code(err), codes.PermissionDenied)
   513  
   514  	// Verify can update project with proper permissions
   515  	_ = appServer.enf.SetBuiltinPolicy(`
   516  p, admin, applications, update, default/test-app, allow
   517  p, admin, applications, create, my-proj/test-app, allow
   518  p, admin, applications, update, my-proj/test-app, allow
   519  `)
   520  	updatedApp, err := appServer.Update(ctx, &application.ApplicationUpdateRequest{Application: testApp})
   521  	assert.NoError(t, err)
   522  	assert.Equal(t, "my-proj", updatedApp.Spec.Project)
   523  }
   524  
   525  func TestAppJsonPatch(t *testing.T) {
   526  	testApp := newTestAppWithAnnotations()
   527  	ctx := context.Background()
   528  	// nolint:staticcheck
   529  	ctx = context.WithValue(ctx, "claims", &jwt.StandardClaims{Subject: "admin"})
   530  	appServer := newTestAppServer(testApp)
   531  	appServer.enf.SetDefaultRole("")
   532  
   533  	app, err := appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: "garbage"})
   534  	assert.Error(t, err)
   535  	assert.Nil(t, app)
   536  
   537  	app, err = appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: "[]"})
   538  	assert.NoError(t, err)
   539  	assert.NotNil(t, app)
   540  
   541  	app, err = appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: `[{"op": "replace", "path": "/spec/source/path", "value": "foo"}]`})
   542  	assert.NoError(t, err)
   543  	assert.Equal(t, "foo", app.Spec.Source.Path)
   544  
   545  	app, err = appServer.Patch(ctx, &application.ApplicationPatchRequest{Name: &testApp.Name, Patch: `[{"op": "remove", "path": "/metadata/annotations/test.annotation"}]`})
   546  	assert.NoError(t, err)
   547  	assert.NotContains(t, app.Annotations, "test.annotation")
   548  }
   549  
   550  func TestAppMergePatch(t *testing.T) {
   551  	testApp := newTestApp()
   552  	ctx := context.Background()
   553  	// nolint:staticcheck
   554  	ctx = context.WithValue(ctx, "claims", &jwt.StandardClaims{Subject: "admin"})
   555  	appServer := newTestAppServer(testApp)
   556  	appServer.enf.SetDefaultRole("")
   557  
   558  	app, err := appServer.Patch(ctx, &application.ApplicationPatchRequest{
   559  		Name: &testApp.Name, Patch: `{"spec": { "source": { "path": "foo" } }}`, PatchType: "merge"})
   560  	assert.NoError(t, err)
   561  	assert.Equal(t, "foo", app.Spec.Source.Path)
   562  }
   563  
   564  func TestServer_GetApplicationSyncWindowsState(t *testing.T) {
   565  	t.Run("Active", func(t *testing.T) {
   566  		testApp := newTestApp()
   567  		testApp.Spec.Project = "proj-maint"
   568  		appServer := newTestAppServer(testApp)
   569  
   570  		active, err := appServer.GetApplicationSyncWindows(context.Background(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name})
   571  		assert.NoError(t, err)
   572  		assert.Equal(t, 1, len(active.ActiveWindows))
   573  	})
   574  	t.Run("Inactive", func(t *testing.T) {
   575  		testApp := newTestApp()
   576  		testApp.Spec.Project = "default"
   577  		appServer := newTestAppServer(testApp)
   578  
   579  		active, err := appServer.GetApplicationSyncWindows(context.Background(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name})
   580  		assert.NoError(t, err)
   581  		assert.Equal(t, 0, len(active.ActiveWindows))
   582  	})
   583  	t.Run("ProjectDoesNotExist", func(t *testing.T) {
   584  		testApp := newTestApp()
   585  		testApp.Spec.Project = "none"
   586  		appServer := newTestAppServer(testApp)
   587  
   588  		active, err := appServer.GetApplicationSyncWindows(context.Background(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name})
   589  		assert.Contains(t, err.Error(), "not found")
   590  		assert.Nil(t, active)
   591  	})
   592  }
   593  
   594  func TestGetCachedAppState(t *testing.T) {
   595  	testApp := newTestApp()
   596  	testApp.ObjectMeta.ResourceVersion = "1"
   597  	testApp.Spec.Project = "none"
   598  	appServer := newTestAppServer(testApp)
   599  
   600  	fakeClientSet := appServer.appclientset.(*apps.Clientset)
   601  
   602  	t.Run("NoError", func(t *testing.T) {
   603  		err := appServer.getCachedAppState(context.Background(), testApp, func() error {
   604  			return nil
   605  		})
   606  		assert.NoError(t, err)
   607  	})
   608  
   609  	t.Run("CacheMissErrorTriggersRefresh", func(t *testing.T) {
   610  		retryCount := 0
   611  		patched := false
   612  		watcher := watch.NewFakeWithChanSize(1, true)
   613  
   614  		// Configure fakeClientSet within lock, before requesting cached app state, to avoid data race
   615  		{
   616  			fakeClientSet.Lock()
   617  			fakeClientSet.ReactionChain = nil
   618  			fakeClientSet.WatchReactionChain = nil
   619  			fakeClientSet.AddReactor("patch", "applications", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
   620  				patched = true
   621  				updated := testApp.DeepCopy()
   622  				updated.ResourceVersion = "2"
   623  				appServer.appBroadcaster.OnUpdate(testApp, updated)
   624  				return true, testApp, nil
   625  			})
   626  			fakeClientSet.AddWatchReactor("applications", func(action kubetesting.Action) (handled bool, ret watch.Interface, err error) {
   627  				return true, watcher, nil
   628  			})
   629  			fakeClientSet.Unlock()
   630  		}
   631  
   632  		err := appServer.getCachedAppState(context.Background(), testApp, func() error {
   633  			res := cache.ErrCacheMiss
   634  			if retryCount == 1 {
   635  				res = nil
   636  			}
   637  			retryCount++
   638  			return res
   639  		})
   640  		assert.Equal(t, nil, err)
   641  		assert.Equal(t, 2, retryCount)
   642  		assert.True(t, patched)
   643  	})
   644  
   645  	t.Run("NonCacheErrorDoesNotTriggerRefresh", func(t *testing.T) {
   646  		randomError := coreerrors.New("random error")
   647  		err := appServer.getCachedAppState(context.Background(), testApp, func() error {
   648  			return randomError
   649  		})
   650  		assert.Equal(t, randomError, err)
   651  	})
   652  }
   653  
   654  func TestSplitStatusPatch(t *testing.T) {
   655  	specPatch := `{"spec":{"aaa":"bbb"}}`
   656  	statusPatch := `{"status":{"ccc":"ddd"}}`
   657  	{
   658  		nonStatus, status, err := splitStatusPatch([]byte(specPatch))
   659  		assert.NoError(t, err)
   660  		assert.Equal(t, specPatch, string(nonStatus))
   661  		assert.Nil(t, status)
   662  	}
   663  	{
   664  		nonStatus, status, err := splitStatusPatch([]byte(statusPatch))
   665  		assert.NoError(t, err)
   666  		assert.Nil(t, nonStatus)
   667  		assert.Equal(t, statusPatch, string(status))
   668  	}
   669  	{
   670  		bothPatch := `{"spec":{"aaa":"bbb"},"status":{"ccc":"ddd"}}`
   671  		nonStatus, status, err := splitStatusPatch([]byte(bothPatch))
   672  		assert.NoError(t, err)
   673  		assert.Equal(t, specPatch, string(nonStatus))
   674  		assert.Equal(t, statusPatch, string(status))
   675  	}
   676  	{
   677  		otherFields := `{"operation":{"eee":"fff"},"spec":{"aaa":"bbb"},"status":{"ccc":"ddd"}}`
   678  		nonStatus, status, err := splitStatusPatch([]byte(otherFields))
   679  		assert.NoError(t, err)
   680  		assert.Equal(t, `{"operation":{"eee":"fff"},"spec":{"aaa":"bbb"}}`, string(nonStatus))
   681  		assert.Equal(t, statusPatch, string(status))
   682  	}
   683  }