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

     1  package hydrator
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"path/filepath"
     7  	"testing"
     8  	"time"
     9  
    10  	log "github.com/sirupsen/logrus"
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/mock"
    13  	"github.com/stretchr/testify/require"
    14  	corev1 "k8s.io/api/core/v1"
    15  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    16  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    17  	"k8s.io/utils/ptr"
    18  
    19  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
    20  
    21  	commitclient "github.com/argoproj/argo-cd/v3/commitserver/apiclient"
    22  	commitservermocks "github.com/argoproj/argo-cd/v3/commitserver/apiclient/mocks"
    23  	"github.com/argoproj/argo-cd/v3/controller/hydrator/mocks"
    24  	"github.com/argoproj/argo-cd/v3/controller/hydrator/types"
    25  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    26  	repoclient "github.com/argoproj/argo-cd/v3/reposerver/apiclient"
    27  	reposervermocks "github.com/argoproj/argo-cd/v3/reposerver/apiclient/mocks"
    28  	"github.com/argoproj/argo-cd/v3/util/settings"
    29  )
    30  
    31  var message = `testn
    32  Argocd-reference-commit-repourl: https://github.com/test/argocd-example-apps
    33  Argocd-reference-commit-author: Argocd-reference-commit-author
    34  Argocd-reference-commit-subject: testhydratormd
    35  Signed-off-by: testUser <test@gmail.com>`
    36  
    37  func Test_appNeedsHydration(t *testing.T) {
    38  	t.Parallel()
    39  
    40  	now := metav1.NewTime(time.Now())
    41  	oneHourAgo := metav1.NewTime(now.Add(-1 * time.Hour))
    42  
    43  	testCases := []struct {
    44  		name                   string
    45  		app                    *v1alpha1.Application
    46  		expectedNeedsHydration bool
    47  		expectedMessage        string
    48  	}{
    49  		{
    50  			name:                   "source hydrator not configured",
    51  			app:                    &v1alpha1.Application{},
    52  			expectedNeedsHydration: false,
    53  			expectedMessage:        "source hydrator not configured",
    54  		},
    55  		{
    56  			name: "no previous hydrate operation",
    57  			app: &v1alpha1.Application{
    58  				Spec: v1alpha1.ApplicationSpec{SourceHydrator: &v1alpha1.SourceHydrator{}},
    59  			},
    60  			expectedNeedsHydration: true,
    61  			expectedMessage:        "no previous hydrate operation",
    62  		},
    63  		{
    64  			name: "operation already in progress",
    65  			app: &v1alpha1.Application{
    66  				Spec:   v1alpha1.ApplicationSpec{SourceHydrator: &v1alpha1.SourceHydrator{}},
    67  				Status: v1alpha1.ApplicationStatus{SourceHydrator: v1alpha1.SourceHydratorStatus{CurrentOperation: &v1alpha1.HydrateOperation{Phase: v1alpha1.HydrateOperationPhaseHydrating}}},
    68  			},
    69  			expectedNeedsHydration: false,
    70  			expectedMessage:        "hydration operation already in progress",
    71  		},
    72  		{
    73  			name: "hydrate requested",
    74  			app: &v1alpha1.Application{
    75  				ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{v1alpha1.AnnotationKeyHydrate: "normal"}},
    76  				Spec:       v1alpha1.ApplicationSpec{SourceHydrator: &v1alpha1.SourceHydrator{}},
    77  				Status:     v1alpha1.ApplicationStatus{SourceHydrator: v1alpha1.SourceHydratorStatus{CurrentOperation: &v1alpha1.HydrateOperation{Phase: v1alpha1.HydrateOperationPhaseHydrated}}},
    78  			},
    79  			expectedNeedsHydration: true,
    80  			expectedMessage:        "hydrate requested",
    81  		},
    82  		{
    83  			name: "spec.sourceHydrator differs",
    84  			app: &v1alpha1.Application{
    85  				Spec: v1alpha1.ApplicationSpec{SourceHydrator: &v1alpha1.SourceHydrator{}},
    86  				Status: v1alpha1.ApplicationStatus{SourceHydrator: v1alpha1.SourceHydratorStatus{CurrentOperation: &v1alpha1.HydrateOperation{
    87  					SourceHydrator: v1alpha1.SourceHydrator{DrySource: v1alpha1.DrySource{RepoURL: "something new"}},
    88  				}}},
    89  			},
    90  			expectedNeedsHydration: true,
    91  			expectedMessage:        "spec.sourceHydrator differs",
    92  		},
    93  		{
    94  			name: "hydration failed more than two minutes ago",
    95  			app: &v1alpha1.Application{
    96  				Spec:   v1alpha1.ApplicationSpec{SourceHydrator: &v1alpha1.SourceHydrator{}},
    97  				Status: v1alpha1.ApplicationStatus{SourceHydrator: v1alpha1.SourceHydratorStatus{CurrentOperation: &v1alpha1.HydrateOperation{DrySHA: "abc123", FinishedAt: &oneHourAgo, Phase: v1alpha1.HydrateOperationPhaseFailed}}},
    98  			},
    99  			expectedNeedsHydration: true,
   100  			expectedMessage:        "previous hydrate operation failed more than 2 minutes ago",
   101  		},
   102  		{
   103  			name: "hydrate not needed",
   104  			app: &v1alpha1.Application{
   105  				Spec:   v1alpha1.ApplicationSpec{SourceHydrator: &v1alpha1.SourceHydrator{}},
   106  				Status: v1alpha1.ApplicationStatus{SourceHydrator: v1alpha1.SourceHydratorStatus{CurrentOperation: &v1alpha1.HydrateOperation{DrySHA: "abc123", StartedAt: now, FinishedAt: &now, Phase: v1alpha1.HydrateOperationPhaseFailed}}},
   107  			},
   108  			expectedNeedsHydration: false,
   109  			expectedMessage:        "hydration not needed",
   110  		},
   111  	}
   112  
   113  	for _, tc := range testCases {
   114  		t.Run(tc.name, func(t *testing.T) {
   115  			t.Parallel()
   116  			needsHydration, message := appNeedsHydration(tc.app)
   117  			assert.Equal(t, tc.expectedNeedsHydration, needsHydration)
   118  			assert.Equal(t, tc.expectedMessage, message)
   119  		})
   120  	}
   121  }
   122  
   123  func Test_getAppsForHydrationKey_RepoURLNormalization(t *testing.T) {
   124  	t.Parallel()
   125  
   126  	d := mocks.NewDependencies(t)
   127  	d.On("GetProcessableApps").Return(&v1alpha1.ApplicationList{
   128  		Items: []v1alpha1.Application{
   129  			{
   130  				Spec: v1alpha1.ApplicationSpec{
   131  					Project: "project",
   132  					SourceHydrator: &v1alpha1.SourceHydrator{
   133  						DrySource: v1alpha1.DrySource{
   134  							RepoURL:        "https://example.com/repo.git",
   135  							TargetRevision: "main",
   136  							Path:           "app1",
   137  						},
   138  						SyncSource: v1alpha1.SyncSource{
   139  							TargetBranch: "main",
   140  							Path:         "app1",
   141  						},
   142  					},
   143  				},
   144  			},
   145  			{
   146  				Spec: v1alpha1.ApplicationSpec{
   147  					Project: "project",
   148  					SourceHydrator: &v1alpha1.SourceHydrator{
   149  						DrySource: v1alpha1.DrySource{
   150  							RepoURL:        "https://example.com/repo",
   151  							TargetRevision: "main",
   152  							Path:           "app2",
   153  						},
   154  						SyncSource: v1alpha1.SyncSource{
   155  							TargetBranch: "main",
   156  							Path:         "app2",
   157  						},
   158  					},
   159  				},
   160  			},
   161  		},
   162  	}, nil)
   163  
   164  	hydrator := &Hydrator{dependencies: d}
   165  
   166  	hydrationKey := types.HydrationQueueKey{
   167  		SourceRepoURL:        "https://example.com/repo",
   168  		SourceTargetRevision: "main",
   169  		DestinationBranch:    "main",
   170  	}
   171  
   172  	apps, err := hydrator.getAppsForHydrationKey(hydrationKey)
   173  
   174  	require.NoError(t, err)
   175  	assert.Len(t, apps, 2, "Expected both apps to be considered relevant despite URL differences")
   176  }
   177  
   178  func TestHydrator_getTemplatedCommitMessage(t *testing.T) {
   179  	references := make([]v1alpha1.RevisionReference, 0)
   180  	revReference := v1alpha1.RevisionReference{
   181  		Commit: &v1alpha1.CommitMetadata{
   182  			Author:  "testAuthor",
   183  			Subject: "test",
   184  			RepoURL: "https://github.com/test/argocd-example-apps",
   185  			SHA:     "3ff41cc5247197a6caf50216c4c76cc29d78a97c",
   186  		},
   187  	}
   188  	references = append(references, revReference)
   189  	type args struct {
   190  		repoURL           string
   191  		revision          string
   192  		dryCommitMetadata *v1alpha1.RevisionMetadata
   193  		template          string
   194  	}
   195  	tests := []struct {
   196  		name    string
   197  		args    args
   198  		want    string
   199  		wantErr bool
   200  	}{
   201  		{
   202  			name: "test template",
   203  			args: args{
   204  				repoURL:  "https://github.com/test/argocd-example-apps",
   205  				revision: "3ff41cc5247197a6caf50216c4c76cc29d78a97d",
   206  				dryCommitMetadata: &v1alpha1.RevisionMetadata{
   207  					Author: "test test@test.com",
   208  					Date: &metav1.Time{
   209  						Time: metav1.Now().Time,
   210  					},
   211  					Message:    message,
   212  					References: references,
   213  				},
   214  				template: settings.CommitMessageTemplate,
   215  			},
   216  			want: `3ff41cc: testn
   217  Argocd-reference-commit-repourl: https://github.com/test/argocd-example-apps
   218  Argocd-reference-commit-author: Argocd-reference-commit-author
   219  Argocd-reference-commit-subject: testhydratormd
   220  Signed-off-by: testUser <test@gmail.com>
   221  
   222  Co-authored-by: testAuthor
   223  Co-authored-by: test test@test.com
   224  `,
   225  		},
   226  		{
   227  			name: "test empty template",
   228  			args: args{
   229  				repoURL:  "https://github.com/test/argocd-example-apps",
   230  				revision: "3ff41cc5247197a6caf50216c4c76cc29d78a97d",
   231  				dryCommitMetadata: &v1alpha1.RevisionMetadata{
   232  					Author: "test test@test.com",
   233  					Date: &metav1.Time{
   234  						Time: metav1.Now().Time,
   235  					},
   236  					Message:    message,
   237  					References: references,
   238  				},
   239  			},
   240  			want: "",
   241  		},
   242  	}
   243  	for _, tt := range tests {
   244  		t.Run(tt.name, func(t *testing.T) {
   245  			got, err := getTemplatedCommitMessage(tt.args.repoURL, tt.args.revision, tt.args.template, tt.args.dryCommitMetadata)
   246  			if (err != nil) != tt.wantErr {
   247  				t.Errorf("Hydrator.getHydratorCommitMessage() error = %v, wantErr %v", err, tt.wantErr)
   248  				return
   249  			}
   250  			assert.Equal(t, tt.want, got)
   251  		})
   252  	}
   253  }
   254  
   255  func Test_validateApplications_RootPathSkipped(t *testing.T) {
   256  	t.Parallel()
   257  
   258  	d := mocks.NewDependencies(t)
   259  	// create an app that has a SyncSource.Path set to root
   260  	apps := []*v1alpha1.Application{
   261  		{
   262  			Spec: v1alpha1.ApplicationSpec{
   263  				Project: "project",
   264  				SourceHydrator: &v1alpha1.SourceHydrator{
   265  					DrySource: v1alpha1.DrySource{
   266  						RepoURL:        "https://example.com/repo",
   267  						TargetRevision: "main",
   268  						Path:           ".", // root path
   269  					},
   270  					SyncSource: v1alpha1.SyncSource{
   271  						TargetBranch: "main",
   272  						Path:         ".", // root path
   273  					},
   274  				},
   275  			},
   276  		},
   277  	}
   278  
   279  	d.On("GetProcessableAppProj", mock.Anything).Return(&v1alpha1.AppProject{
   280  		Spec: v1alpha1.AppProjectSpec{
   281  			SourceRepos: []string{"https://example.com/*"},
   282  		},
   283  	}, nil).Maybe()
   284  
   285  	hydrator := &Hydrator{dependencies: d}
   286  
   287  	proj, errors := hydrator.validateApplications(apps)
   288  	require.Len(t, errors, 1)
   289  	require.ErrorContains(t, errors[apps[0].QualifiedName()], "app is configured to hydrate to the repository root")
   290  	assert.Nil(t, proj)
   291  }
   292  
   293  func TestIsRootPath(t *testing.T) {
   294  	tests := []struct {
   295  		name     string
   296  		path     string
   297  		expected bool
   298  	}{
   299  		{"empty string", "", true},
   300  		{"dot path", ".", true},
   301  		{"slash", string(filepath.Separator), true},
   302  		{"nested path", "app", false},
   303  		{"nested path with slash", "app/", false},
   304  		{"deep path", "app/config", false},
   305  		{"current dir with trailing slash", "./", true},
   306  	}
   307  	for _, tt := range tests {
   308  		t.Run(tt.name, func(t *testing.T) {
   309  			result := IsRootPath(tt.path)
   310  			require.Equal(t, tt.expected, result)
   311  		})
   312  	}
   313  }
   314  
   315  func newTestProject() *v1alpha1.AppProject {
   316  	return &v1alpha1.AppProject{
   317  		ObjectMeta: metav1.ObjectMeta{Name: "test-project", Namespace: "default"},
   318  		Spec: v1alpha1.AppProjectSpec{
   319  			SourceRepos: []string{"https://example.com/repo"},
   320  		},
   321  	}
   322  }
   323  
   324  func newTestApp(name string) *v1alpha1.Application {
   325  	app := &v1alpha1.Application{
   326  		ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "default"},
   327  		Spec: v1alpha1.ApplicationSpec{
   328  			Project: "test-project",
   329  			SourceHydrator: &v1alpha1.SourceHydrator{
   330  				DrySource: v1alpha1.DrySource{
   331  					RepoURL:        "https://example.com/repo",
   332  					TargetRevision: "main",
   333  					Path:           "base/app",
   334  				},
   335  				SyncSource: v1alpha1.SyncSource{
   336  					TargetBranch: "hydrated",
   337  					Path:         "app",
   338  				},
   339  				HydrateTo: &v1alpha1.HydrateTo{
   340  					TargetBranch: "hydrated-next",
   341  				},
   342  			},
   343  		},
   344  	}
   345  	return app
   346  }
   347  
   348  func setTestAppPhase(app *v1alpha1.Application, phase v1alpha1.HydrateOperationPhase) *v1alpha1.Application {
   349  	status := v1alpha1.SourceHydratorStatus{}
   350  	switch phase {
   351  	case v1alpha1.HydrateOperationPhaseHydrating:
   352  		status = v1alpha1.SourceHydratorStatus{
   353  			CurrentOperation: &v1alpha1.HydrateOperation{
   354  				StartedAt:      metav1.Now(),
   355  				FinishedAt:     nil,
   356  				Phase:          phase,
   357  				SourceHydrator: *app.Spec.SourceHydrator,
   358  			},
   359  		}
   360  	case v1alpha1.HydrateOperationPhaseFailed:
   361  		status = v1alpha1.SourceHydratorStatus{
   362  			CurrentOperation: &v1alpha1.HydrateOperation{
   363  				StartedAt:      metav1.Now(),
   364  				FinishedAt:     ptr.To(metav1.Now()),
   365  				Phase:          phase,
   366  				Message:        "some error",
   367  				SourceHydrator: *app.Spec.SourceHydrator,
   368  			},
   369  		}
   370  
   371  	case v1alpha1.HydrateOperationPhaseHydrated:
   372  		status = v1alpha1.SourceHydratorStatus{
   373  			CurrentOperation: &v1alpha1.HydrateOperation{
   374  				StartedAt:      metav1.Now(),
   375  				FinishedAt:     ptr.To(metav1.Now()),
   376  				Phase:          phase,
   377  				DrySHA:         "12345",
   378  				HydratedSHA:    "67890",
   379  				SourceHydrator: *app.Spec.SourceHydrator,
   380  			},
   381  		}
   382  	}
   383  
   384  	app.Status.SourceHydrator = status
   385  	return app
   386  }
   387  
   388  func TestProcessAppHydrateQueueItem_HydrationNeeded(t *testing.T) {
   389  	t.Parallel()
   390  	d := mocks.NewDependencies(t)
   391  	app := newTestApp("test-app")
   392  
   393  	// appNeedsHydration returns true if no CurrentOperation
   394  	app.Status.SourceHydrator.CurrentOperation = nil
   395  
   396  	var persistedStatus *v1alpha1.SourceHydratorStatus
   397  	d.On("PersistAppHydratorStatus", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
   398  		persistedStatus = args.Get(1).(*v1alpha1.SourceHydratorStatus)
   399  	}).Return().Once()
   400  	d.On("AddHydrationQueueItem", mock.Anything).Return().Once()
   401  
   402  	h := &Hydrator{
   403  		dependencies:         d,
   404  		statusRefreshTimeout: time.Minute,
   405  	}
   406  
   407  	h.ProcessAppHydrateQueueItem(app)
   408  
   409  	d.AssertCalled(t, "PersistAppHydratorStatus", mock.Anything, mock.Anything)
   410  	d.AssertCalled(t, "AddHydrationQueueItem", mock.Anything)
   411  
   412  	require.NotNil(t, persistedStatus)
   413  	assert.NotNil(t, persistedStatus.CurrentOperation.StartedAt)
   414  	assert.Nil(t, persistedStatus.CurrentOperation.FinishedAt)
   415  	assert.Equal(t, v1alpha1.HydrateOperationPhaseHydrating, persistedStatus.CurrentOperation.Phase)
   416  	assert.Equal(t, *app.Spec.SourceHydrator, persistedStatus.CurrentOperation.SourceHydrator)
   417  }
   418  
   419  func TestProcessAppHydrateQueueItem_HydrationPassedTimeout(t *testing.T) {
   420  	t.Parallel()
   421  	d := mocks.NewDependencies(t)
   422  	now := metav1.Now()
   423  	// StartedAt is more than statusRefreshTimeout ago
   424  	startedAt := metav1.NewTime(now.Add(-2 * time.Minute))
   425  	app := newTestApp("test-app")
   426  	app.Status = v1alpha1.ApplicationStatus{
   427  		SourceHydrator: v1alpha1.SourceHydratorStatus{
   428  			CurrentOperation: &v1alpha1.HydrateOperation{
   429  				StartedAt:      startedAt,
   430  				Phase:          v1alpha1.HydrateOperationPhaseHydrating,
   431  				SourceHydrator: v1alpha1.SourceHydrator{},
   432  			},
   433  		},
   434  	}
   435  	d.On("AddHydrationQueueItem", mock.Anything).Return().Once()
   436  
   437  	h := &Hydrator{
   438  		dependencies:         d,
   439  		statusRefreshTimeout: time.Minute,
   440  	}
   441  
   442  	h.ProcessAppHydrateQueueItem(app)
   443  
   444  	d.AssertCalled(t, "AddHydrationQueueItem", mock.Anything)
   445  	d.AssertNotCalled(t, "PersistAppHydratorStatus", mock.Anything, mock.Anything)
   446  }
   447  
   448  func TestProcessAppHydrateQueueItem_NoSourceHydrator(t *testing.T) {
   449  	t.Parallel()
   450  	d := mocks.NewDependencies(t)
   451  	app := newTestApp("test-app")
   452  	app.Spec.SourceHydrator = nil
   453  
   454  	h := &Hydrator{
   455  		dependencies:         d,
   456  		statusRefreshTimeout: time.Minute,
   457  	}
   458  	h.ProcessAppHydrateQueueItem(app)
   459  
   460  	// Should not call anything
   461  	d.AssertNotCalled(t, "PersistAppHydratorStatus", mock.Anything, mock.Anything)
   462  	d.AssertNotCalled(t, "AddHydrationQueueItem", mock.Anything)
   463  }
   464  
   465  func TestProcessAppHydrateQueueItem_HydrationNotNeeded(t *testing.T) {
   466  	t.Parallel()
   467  	d := mocks.NewDependencies(t)
   468  	now := metav1.Now()
   469  	app := newTestApp("test-app")
   470  	app.Status = v1alpha1.ApplicationStatus{
   471  		SourceHydrator: v1alpha1.SourceHydratorStatus{
   472  			CurrentOperation: &v1alpha1.HydrateOperation{
   473  				StartedAt: now,
   474  				Phase:     v1alpha1.HydrateOperationPhaseHydrating,
   475  			},
   476  		},
   477  	}
   478  
   479  	h := &Hydrator{
   480  		dependencies:         d,
   481  		statusRefreshTimeout: time.Minute,
   482  	}
   483  	h.ProcessAppHydrateQueueItem(app)
   484  
   485  	// Should not call anything
   486  	d.AssertNotCalled(t, "PersistAppHydratorStatus", mock.Anything, mock.Anything)
   487  	d.AssertNotCalled(t, "AddHydrationQueueItem", mock.Anything)
   488  }
   489  
   490  func TestProcessHydrationQueueItem_ValidationFails(t *testing.T) {
   491  	t.Parallel()
   492  	d := mocks.NewDependencies(t)
   493  	app1 := setTestAppPhase(newTestApp("test-app"), v1alpha1.HydrateOperationPhaseHydrating)
   494  	app2 := setTestAppPhase(newTestApp("test-app-2"), v1alpha1.HydrateOperationPhaseHydrating)
   495  	hydrationKey := getHydrationQueueKey(app1)
   496  
   497  	// getAppsForHydrationKey returns two apps
   498  	d.On("GetProcessableApps").Return(&v1alpha1.ApplicationList{Items: []v1alpha1.Application{*app1, *app2}}, nil)
   499  	d.On("GetProcessableAppProj", mock.Anything).Return(nil, errors.New("test error")).Once()
   500  	d.On("GetProcessableAppProj", mock.Anything).Return(newTestProject(), nil).Once()
   501  
   502  	h := &Hydrator{dependencies: d}
   503  
   504  	// Expect setAppHydratorError to be called
   505  	var persistedStatus1 *v1alpha1.SourceHydratorStatus
   506  	var persistedStatus2 *v1alpha1.SourceHydratorStatus
   507  	d.On("PersistAppHydratorStatus", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
   508  		if args.Get(0).(*v1alpha1.Application).Name == app1.Name {
   509  			persistedStatus1 = args.Get(1).(*v1alpha1.SourceHydratorStatus)
   510  		} else if args.Get(0).(*v1alpha1.Application).Name == app2.Name {
   511  			persistedStatus2 = args.Get(1).(*v1alpha1.SourceHydratorStatus)
   512  		}
   513  	}).Return().Twice()
   514  
   515  	h.ProcessHydrationQueueItem(hydrationKey)
   516  
   517  	assert.NotNil(t, persistedStatus1)
   518  	assert.NotNil(t, persistedStatus1.CurrentOperation.FinishedAt)
   519  	assert.Contains(t, persistedStatus1.CurrentOperation.Message, "test error")
   520  	assert.Equal(t, v1alpha1.HydrateOperationPhaseFailed, persistedStatus1.CurrentOperation.Phase)
   521  	assert.NotNil(t, persistedStatus2)
   522  	assert.NotNil(t, persistedStatus2.CurrentOperation.FinishedAt)
   523  	assert.Contains(t, persistedStatus2.CurrentOperation.Message, "cannot hydrate because application default/test-app has an error")
   524  	assert.Equal(t, v1alpha1.HydrateOperationPhaseFailed, persistedStatus1.CurrentOperation.Phase)
   525  
   526  	d.AssertNumberOfCalls(t, "PersistAppHydratorStatus", 2)
   527  	d.AssertNotCalled(t, "RequestAppRefresh", mock.Anything, mock.Anything)
   528  }
   529  
   530  func TestProcessHydrationQueueItem_HydrateFails_AppSpecificError(t *testing.T) {
   531  	t.Parallel()
   532  	d := mocks.NewDependencies(t)
   533  	app1 := setTestAppPhase(newTestApp("test-app"), v1alpha1.HydrateOperationPhaseHydrating)
   534  	app2 := newTestApp("test-app-2")
   535  	app2.Spec.SourceHydrator.SyncSource.Path = "something/else"
   536  	app2 = setTestAppPhase(app2, v1alpha1.HydrateOperationPhaseHydrating)
   537  	hydrationKey := getHydrationQueueKey(app1)
   538  
   539  	d.On("GetProcessableApps").Return(&v1alpha1.ApplicationList{Items: []v1alpha1.Application{*app1, *app2}}, nil)
   540  	d.On("GetProcessableAppProj", mock.Anything).Return(newTestProject(), nil)
   541  
   542  	h := &Hydrator{dependencies: d}
   543  
   544  	// Make hydrate return app-specific error
   545  	d.On("GetRepoObjs", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil, errors.New("hydrate error"))
   546  
   547  	// Expect setAppHydratorError to be called
   548  	var persistedStatus1 *v1alpha1.SourceHydratorStatus
   549  	var persistedStatus2 *v1alpha1.SourceHydratorStatus
   550  	d.On("PersistAppHydratorStatus", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
   551  		if args.Get(0).(*v1alpha1.Application).Name == app1.Name {
   552  			persistedStatus1 = args.Get(1).(*v1alpha1.SourceHydratorStatus)
   553  		} else if args.Get(0).(*v1alpha1.Application).Name == app2.Name {
   554  			persistedStatus2 = args.Get(1).(*v1alpha1.SourceHydratorStatus)
   555  		}
   556  	}).Return().Twice()
   557  
   558  	h.ProcessHydrationQueueItem(hydrationKey)
   559  
   560  	assert.NotNil(t, persistedStatus1)
   561  	assert.NotNil(t, persistedStatus1.CurrentOperation.FinishedAt)
   562  	assert.Contains(t, persistedStatus1.CurrentOperation.Message, "hydrate error")
   563  	assert.Equal(t, v1alpha1.HydrateOperationPhaseFailed, persistedStatus1.CurrentOperation.Phase)
   564  	assert.NotNil(t, persistedStatus2)
   565  	assert.NotNil(t, persistedStatus2.CurrentOperation.FinishedAt)
   566  	assert.Contains(t, persistedStatus2.CurrentOperation.Message, "cannot hydrate because application default/test-app has an error")
   567  	assert.Equal(t, v1alpha1.HydrateOperationPhaseFailed, persistedStatus1.CurrentOperation.Phase)
   568  
   569  	d.AssertNumberOfCalls(t, "PersistAppHydratorStatus", 2)
   570  	d.AssertNotCalled(t, "RequestAppRefresh", mock.Anything, mock.Anything)
   571  }
   572  
   573  func TestProcessHydrationQueueItem_HydrateFails_CommonError(t *testing.T) {
   574  	t.Parallel()
   575  	d := mocks.NewDependencies(t)
   576  	r := mocks.NewRepoGetter(t)
   577  	app1 := setTestAppPhase(newTestApp("test-app"), v1alpha1.HydrateOperationPhaseHydrating)
   578  	app2 := newTestApp("test-app-2")
   579  	app2.Spec.SourceHydrator.SyncSource.Path = "something/else"
   580  	app2 = setTestAppPhase(app2, v1alpha1.HydrateOperationPhaseHydrating)
   581  	hydrationKey := getHydrationQueueKey(app1)
   582  	d.On("GetProcessableApps").Return(&v1alpha1.ApplicationList{Items: []v1alpha1.Application{*app1, *app2}}, nil)
   583  	d.On("GetProcessableAppProj", mock.Anything).Return(newTestProject(), nil)
   584  	h := &Hydrator{dependencies: d, repoGetter: r}
   585  
   586  	d.On("GetRepoObjs", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, &repoclient.ManifestResponse{
   587  		Revision: "abc123",
   588  	}, nil)
   589  	r.On("GetRepository", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("repo error"))
   590  
   591  	// Expect setAppHydratorError to be called
   592  	var persistedStatus1 *v1alpha1.SourceHydratorStatus
   593  	var persistedStatus2 *v1alpha1.SourceHydratorStatus
   594  	d.On("PersistAppHydratorStatus", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
   595  		if args.Get(0).(*v1alpha1.Application).Name == app1.Name {
   596  			persistedStatus1 = args.Get(1).(*v1alpha1.SourceHydratorStatus)
   597  		} else if args.Get(0).(*v1alpha1.Application).Name == app2.Name {
   598  			persistedStatus2 = args.Get(1).(*v1alpha1.SourceHydratorStatus)
   599  		}
   600  	}).Return().Twice()
   601  
   602  	h.ProcessHydrationQueueItem(hydrationKey)
   603  
   604  	assert.NotNil(t, persistedStatus1)
   605  	assert.NotNil(t, persistedStatus1.CurrentOperation.FinishedAt)
   606  	assert.Contains(t, persistedStatus1.CurrentOperation.Message, "repo error")
   607  	assert.Equal(t, v1alpha1.HydrateOperationPhaseFailed, persistedStatus1.CurrentOperation.Phase)
   608  	assert.Equal(t, "abc123", persistedStatus1.CurrentOperation.DrySHA)
   609  	assert.NotNil(t, persistedStatus2)
   610  	assert.NotNil(t, persistedStatus2.CurrentOperation.FinishedAt)
   611  	assert.Contains(t, persistedStatus2.CurrentOperation.Message, "repo error")
   612  	assert.Equal(t, v1alpha1.HydrateOperationPhaseFailed, persistedStatus1.CurrentOperation.Phase)
   613  	assert.Equal(t, "abc123", persistedStatus1.CurrentOperation.DrySHA)
   614  
   615  	d.AssertNumberOfCalls(t, "PersistAppHydratorStatus", 2)
   616  	d.AssertNotCalled(t, "RequestAppRefresh", mock.Anything, mock.Anything)
   617  }
   618  
   619  func TestProcessHydrationQueueItem_SuccessfulHydration(t *testing.T) {
   620  	t.Parallel()
   621  	d := mocks.NewDependencies(t)
   622  	r := mocks.NewRepoGetter(t)
   623  	rc := reposervermocks.NewRepoServerServiceClient(t)
   624  	cc := commitservermocks.NewCommitServiceClient(t)
   625  	app := setTestAppPhase(newTestApp("test-app"), v1alpha1.HydrateOperationPhaseHydrating)
   626  	hydrationKey := getHydrationQueueKey(app)
   627  	d.On("GetProcessableApps").Return(&v1alpha1.ApplicationList{Items: []v1alpha1.Application{*app}}, nil)
   628  	d.On("GetProcessableAppProj", mock.Anything).Return(newTestProject(), nil)
   629  	h := &Hydrator{dependencies: d, repoGetter: r, commitClientset: &commitservermocks.Clientset{CommitServiceClient: cc}, repoClientset: &reposervermocks.Clientset{RepoServerServiceClient: rc}}
   630  
   631  	// Expect setAppHydratorError to be called
   632  	var persistedStatus *v1alpha1.SourceHydratorStatus
   633  	d.On("PersistAppHydratorStatus", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
   634  		persistedStatus = args.Get(1).(*v1alpha1.SourceHydratorStatus)
   635  	}).Return().Once()
   636  	d.On("RequestAppRefresh", app.Name, app.Namespace).Return(nil).Once()
   637  	d.On("GetRepoObjs", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, &repoclient.ManifestResponse{
   638  		Revision: "abc123",
   639  	}, nil).Once()
   640  	r.On("GetRepository", mock.Anything, "https://example.com/repo", "test-project").Return(nil, nil).Once()
   641  	rc.On("GetRevisionMetadata", mock.Anything, mock.Anything).Return(nil, nil).Once()
   642  	d.On("GetWriteCredentials", mock.Anything, "https://example.com/repo", "test-project").Return(nil, nil).Once()
   643  	d.On("GetHydratorCommitMessageTemplate").Return("commit message", nil).Once()
   644  	cc.On("CommitHydratedManifests", mock.Anything, mock.Anything).Return(&commitclient.CommitHydratedManifestsResponse{HydratedSha: "def456"}, nil).Once()
   645  
   646  	h.ProcessHydrationQueueItem(hydrationKey)
   647  
   648  	d.AssertCalled(t, "PersistAppHydratorStatus", mock.Anything, mock.Anything)
   649  	d.AssertCalled(t, "RequestAppRefresh", app.Name, app.Namespace)
   650  	assert.NotNil(t, persistedStatus)
   651  	assert.Equal(t, app.Status.SourceHydrator.CurrentOperation.StartedAt, persistedStatus.CurrentOperation.StartedAt)
   652  	assert.Equal(t, app.Status.SourceHydrator.CurrentOperation.SourceHydrator, persistedStatus.CurrentOperation.SourceHydrator)
   653  	assert.NotNil(t, persistedStatus.CurrentOperation.FinishedAt)
   654  	assert.Equal(t, v1alpha1.HydrateOperationPhaseHydrated, persistedStatus.CurrentOperation.Phase)
   655  	assert.Empty(t, persistedStatus.CurrentOperation.Message)
   656  	assert.Equal(t, "abc123", persistedStatus.CurrentOperation.DrySHA)
   657  	assert.Equal(t, "def456", persistedStatus.CurrentOperation.HydratedSHA)
   658  	assert.NotNil(t, persistedStatus.LastSuccessfulOperation)
   659  	assert.Equal(t, "abc123", persistedStatus.LastSuccessfulOperation.DrySHA)
   660  	assert.Equal(t, "def456", persistedStatus.LastSuccessfulOperation.HydratedSHA)
   661  	assert.Equal(t, app.Status.SourceHydrator.CurrentOperation.SourceHydrator, persistedStatus.LastSuccessfulOperation.SourceHydrator)
   662  }
   663  
   664  func TestValidateApplications_ProjectError(t *testing.T) {
   665  	t.Parallel()
   666  	d := mocks.NewDependencies(t)
   667  	app := newTestApp("test-app")
   668  	d.On("GetProcessableAppProj", app).Return(nil, errors.New("project error")).Once()
   669  	h := &Hydrator{dependencies: d}
   670  
   671  	projects, errs := h.validateApplications([]*v1alpha1.Application{app})
   672  	require.Nil(t, projects)
   673  	require.Len(t, errs, 1)
   674  	require.ErrorContains(t, errs[app.QualifiedName()], "project error")
   675  }
   676  
   677  func TestValidateApplications_SourceNotPermitted(t *testing.T) {
   678  	t.Parallel()
   679  	d := mocks.NewDependencies(t)
   680  	app := newTestApp("test-app")
   681  	proj := newTestProject()
   682  	proj.Spec.SourceRepos = []string{"not-allowed"}
   683  	d.On("GetProcessableAppProj", app).Return(proj, nil).Once()
   684  	h := &Hydrator{dependencies: d}
   685  
   686  	projects, errs := h.validateApplications([]*v1alpha1.Application{app})
   687  	require.Nil(t, projects)
   688  	require.Len(t, errs, 1)
   689  	require.ErrorContains(t, errs[app.QualifiedName()], "application repo https://example.com/repo is not permitted in project 'test-project'")
   690  }
   691  
   692  func TestValidateApplications_RootPath(t *testing.T) {
   693  	t.Parallel()
   694  	d := mocks.NewDependencies(t)
   695  	app := newTestApp("test-app")
   696  	app.Spec.SourceHydrator.SyncSource.Path = "."
   697  	proj := newTestProject()
   698  	d.On("GetProcessableAppProj", app).Return(proj, nil).Once()
   699  	h := &Hydrator{dependencies: d}
   700  
   701  	projects, errs := h.validateApplications([]*v1alpha1.Application{app})
   702  	require.Nil(t, projects)
   703  	require.Len(t, errs, 1)
   704  	require.ErrorContains(t, errs[app.QualifiedName()], "app is configured to hydrate to the repository root")
   705  }
   706  
   707  func TestValidateApplications_DuplicateDestination(t *testing.T) {
   708  	t.Parallel()
   709  	d := mocks.NewDependencies(t)
   710  	app1 := newTestApp("app1")
   711  	app2 := newTestApp("app2")
   712  	app2.Spec.SourceHydrator.SyncSource.Path = app1.Spec.SourceHydrator.SyncSource.Path // duplicate path
   713  	proj := newTestProject()
   714  	d.On("GetProcessableAppProj", app1).Return(proj, nil).Once()
   715  	d.On("GetProcessableAppProj", app2).Return(proj, nil).Once()
   716  	h := &Hydrator{dependencies: d}
   717  
   718  	projects, errs := h.validateApplications([]*v1alpha1.Application{app1, app2})
   719  	require.Nil(t, projects)
   720  	require.Len(t, errs, 2)
   721  	require.ErrorContains(t, errs[app1.QualifiedName()], "app default/app2 hydrator use the same destination")
   722  	require.ErrorContains(t, errs[app2.QualifiedName()], "app default/app1 hydrator use the same destination")
   723  }
   724  
   725  func TestValidateApplications_Success(t *testing.T) {
   726  	t.Parallel()
   727  	d := mocks.NewDependencies(t)
   728  	app1 := newTestApp("app1")
   729  	app2 := newTestApp("app2")
   730  	app2.Spec.SourceHydrator.SyncSource.Path = "other-path"
   731  	proj := newTestProject()
   732  	d.On("GetProcessableAppProj", app1).Return(proj, nil).Once()
   733  	d.On("GetProcessableAppProj", app2).Return(proj, nil).Once()
   734  	h := &Hydrator{dependencies: d}
   735  
   736  	projects, errs := h.validateApplications([]*v1alpha1.Application{app1, app2})
   737  	require.NotNil(t, projects)
   738  	require.Empty(t, errs)
   739  	assert.Equal(t, proj, projects[app1.Spec.Project])
   740  	assert.Equal(t, proj, projects[app2.Spec.Project])
   741  }
   742  
   743  func TestGenericHydrationError(t *testing.T) {
   744  	t.Run("no errors", func(t *testing.T) {
   745  		err := genericHydrationError(map[string]error{})
   746  		assert.NoError(t, err)
   747  	})
   748  
   749  	t.Run("single error", func(t *testing.T) {
   750  		errs := map[string]error{
   751  			"default/app1": errors.New("error1"),
   752  		}
   753  		err := genericHydrationError(errs)
   754  		require.Error(t, err)
   755  		assert.Equal(t, "cannot hydrate because application default/app1 has an error", err.Error())
   756  	})
   757  
   758  	t.Run("multiple errors", func(t *testing.T) {
   759  		errs := map[string]error{
   760  			"default/app1": errors.New("error1"),
   761  			"default/app2": errors.New("error2"),
   762  			"default/app3": errors.New("error3"),
   763  		}
   764  		err := genericHydrationError(errs)
   765  		require.Error(t, err)
   766  		// Sorted keys, so default/app1 is first
   767  		assert.Equal(t, "cannot hydrate because application default/app1 and 2 more have errors", err.Error())
   768  	})
   769  }
   770  
   771  func TestHydrator_hydrate_Success(t *testing.T) {
   772  	t.Parallel()
   773  
   774  	d := mocks.NewDependencies(t)
   775  	r := mocks.NewRepoGetter(t)
   776  	cc := commitservermocks.NewCommitServiceClient(t)
   777  	rc := reposervermocks.NewRepoServerServiceClient(t)
   778  	h := &Hydrator{
   779  		dependencies:    d,
   780  		repoGetter:      r,
   781  		repoClientset:   &reposervermocks.Clientset{RepoServerServiceClient: rc},
   782  		commitClientset: &commitservermocks.Clientset{CommitServiceClient: cc},
   783  	}
   784  
   785  	app1 := newTestApp("app1")
   786  	app2 := newTestApp("app2")
   787  	app2.Spec.SourceHydrator.SyncSource.Path = "other-path"
   788  	apps := []*v1alpha1.Application{app1, app2}
   789  	proj := newTestProject()
   790  	projects := map[string]*v1alpha1.AppProject{app1.Spec.Project: proj}
   791  	readRepo := &v1alpha1.Repository{Repo: "https://example.com/repo"}
   792  	writeRepo := &v1alpha1.Repository{Repo: "https://example.com/repo"}
   793  
   794  	d.On("GetRepoObjs", mock.Anything, app1, app1.Spec.SourceHydrator.GetDrySource(), "main", proj).Return(nil, &repoclient.ManifestResponse{Revision: "sha123"}, nil)
   795  	d.On("GetRepoObjs", mock.Anything, app2, app2.Spec.SourceHydrator.GetDrySource(), "sha123", proj).Return(nil, &repoclient.ManifestResponse{Revision: "sha123"}, nil)
   796  	r.On("GetRepository", mock.Anything, readRepo.Repo, proj.Name).Return(readRepo, nil)
   797  	rc.On("GetRevisionMetadata", mock.Anything, mock.Anything).Return(&v1alpha1.RevisionMetadata{Message: "metadata"}, nil).Run(func(args mock.Arguments) {
   798  		r := args.Get(1).(*repoclient.RepoServerRevisionMetadataRequest)
   799  		assert.Equal(t, readRepo, r.Repo)
   800  		assert.Equal(t, "sha123", r.Revision)
   801  	})
   802  	d.On("GetWriteCredentials", mock.Anything, readRepo.Repo, proj.Name).Return(writeRepo, nil)
   803  	d.On("GetHydratorCommitMessageTemplate").Return("commit message", nil)
   804  	cc.On("CommitHydratedManifests", mock.Anything, mock.Anything).Return(&commitclient.CommitHydratedManifestsResponse{HydratedSha: "hydrated123"}, nil).Run(func(args mock.Arguments) {
   805  		r := args.Get(1).(*commitclient.CommitHydratedManifestsRequest)
   806  		assert.Equal(t, "commit message", r.CommitMessage)
   807  		assert.Equal(t, "hydrated", r.SyncBranch)
   808  		assert.Equal(t, "hydrated-next", r.TargetBranch)
   809  		assert.Equal(t, "sha123", r.DrySha)
   810  		assert.Equal(t, writeRepo, r.Repo)
   811  		assert.Len(t, r.Paths, 2)
   812  		assert.Equal(t, app1.Spec.SourceHydrator.SyncSource.Path, r.Paths[0].Path)
   813  		assert.Equal(t, app2.Spec.SourceHydrator.SyncSource.Path, r.Paths[1].Path)
   814  		assert.Equal(t, "metadata", r.DryCommitMetadata.Message)
   815  	})
   816  	logCtx := log.NewEntry(log.StandardLogger())
   817  
   818  	sha, hydratedSha, errs, err := h.hydrate(logCtx, apps, projects)
   819  
   820  	require.NoError(t, err)
   821  	assert.Equal(t, "sha123", sha)
   822  	assert.Equal(t, "hydrated123", hydratedSha)
   823  	assert.Empty(t, errs)
   824  }
   825  
   826  func TestHydrator_hydrate_GetManifestsError(t *testing.T) {
   827  	t.Parallel()
   828  
   829  	d := mocks.NewDependencies(t)
   830  	r := mocks.NewRepoGetter(t)
   831  	cc := commitservermocks.NewCommitServiceClient(t)
   832  	rc := reposervermocks.NewRepoServerServiceClient(t)
   833  	h := &Hydrator{
   834  		dependencies:    d,
   835  		repoGetter:      r,
   836  		repoClientset:   &reposervermocks.Clientset{RepoServerServiceClient: rc},
   837  		commitClientset: &commitservermocks.Clientset{CommitServiceClient: cc},
   838  	}
   839  
   840  	app := newTestApp("app1")
   841  	proj := newTestProject()
   842  	projects := map[string]*v1alpha1.AppProject{app.Spec.Project: proj}
   843  
   844  	d.On("GetRepoObjs", mock.Anything, app, mock.Anything, mock.Anything, proj).Return(nil, nil, errors.New("manifests error"))
   845  	logCtx := log.NewEntry(log.StandardLogger())
   846  
   847  	sha, hydratedSha, errs, err := h.hydrate(logCtx, []*v1alpha1.Application{app}, projects)
   848  
   849  	require.NoError(t, err)
   850  	assert.Empty(t, sha)
   851  	assert.Empty(t, hydratedSha)
   852  	require.Len(t, errs, 1)
   853  	assert.ErrorContains(t, errs[app.QualifiedName()], "manifests error")
   854  }
   855  
   856  func TestHydrator_hydrate_RevisionMetadataError(t *testing.T) {
   857  	t.Parallel()
   858  
   859  	d := mocks.NewDependencies(t)
   860  	r := mocks.NewRepoGetter(t)
   861  	cc := commitservermocks.NewCommitServiceClient(t)
   862  	rc := reposervermocks.NewRepoServerServiceClient(t)
   863  	h := &Hydrator{
   864  		dependencies:    d,
   865  		repoGetter:      r,
   866  		repoClientset:   &reposervermocks.Clientset{RepoServerServiceClient: rc},
   867  		commitClientset: &commitservermocks.Clientset{CommitServiceClient: cc},
   868  	}
   869  
   870  	app := newTestApp("app1")
   871  	proj := newTestProject()
   872  	projects := map[string]*v1alpha1.AppProject{app.Spec.Project: proj}
   873  
   874  	d.On("GetRepoObjs", mock.Anything, app, mock.Anything, mock.Anything, proj).Return(nil, &repoclient.ManifestResponse{Revision: "sha123"}, nil)
   875  	r.On("GetRepository", mock.Anything, mock.Anything, mock.Anything).Return(&v1alpha1.Repository{Repo: "https://example.com/repo"}, nil)
   876  	rc.On("GetRevisionMetadata", mock.Anything, mock.Anything).Return(nil, errors.New("metadata error"))
   877  	logCtx := log.NewEntry(log.StandardLogger())
   878  
   879  	sha, hydratedSha, errs, err := h.hydrate(logCtx, []*v1alpha1.Application{app}, projects)
   880  
   881  	require.Error(t, err)
   882  	assert.Equal(t, "sha123", sha)
   883  	assert.Empty(t, hydratedSha)
   884  	assert.Empty(t, errs)
   885  	assert.ErrorContains(t, err, "metadata error")
   886  }
   887  
   888  func TestHydrator_hydrate_GetWriteCredentialsError(t *testing.T) {
   889  	t.Parallel()
   890  
   891  	d := mocks.NewDependencies(t)
   892  	r := mocks.NewRepoGetter(t)
   893  	cc := commitservermocks.NewCommitServiceClient(t)
   894  	rc := reposervermocks.NewRepoServerServiceClient(t)
   895  	h := &Hydrator{
   896  		dependencies:    d,
   897  		repoGetter:      r,
   898  		repoClientset:   &reposervermocks.Clientset{RepoServerServiceClient: rc},
   899  		commitClientset: &commitservermocks.Clientset{CommitServiceClient: cc},
   900  	}
   901  
   902  	app := newTestApp("app1")
   903  	proj := newTestProject()
   904  	projects := map[string]*v1alpha1.AppProject{app.Spec.Project: proj}
   905  
   906  	d.On("GetRepoObjs", mock.Anything, app, mock.Anything, mock.Anything, proj).Return(nil, &repoclient.ManifestResponse{Revision: "sha123"}, nil)
   907  	r.On("GetRepository", mock.Anything, mock.Anything, mock.Anything).Return(&v1alpha1.Repository{Repo: "https://example.com/repo"}, nil)
   908  	rc.On("GetRevisionMetadata", mock.Anything, mock.Anything).Return(&v1alpha1.RevisionMetadata{}, nil)
   909  	d.On("GetWriteCredentials", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("creds error"))
   910  	logCtx := log.NewEntry(log.StandardLogger())
   911  
   912  	sha, hydratedSha, errs, err := h.hydrate(logCtx, []*v1alpha1.Application{app}, projects)
   913  
   914  	require.Error(t, err)
   915  	assert.Equal(t, "sha123", sha)
   916  	assert.Empty(t, hydratedSha)
   917  	assert.Empty(t, errs)
   918  	assert.ErrorContains(t, err, "creds error")
   919  }
   920  
   921  func TestHydrator_hydrate_CommitMessageTemplateError(t *testing.T) {
   922  	t.Parallel()
   923  
   924  	d := mocks.NewDependencies(t)
   925  	r := mocks.NewRepoGetter(t)
   926  	cc := commitservermocks.NewCommitServiceClient(t)
   927  	rc := reposervermocks.NewRepoServerServiceClient(t)
   928  	h := &Hydrator{
   929  		dependencies:    d,
   930  		repoGetter:      r,
   931  		repoClientset:   &reposervermocks.Clientset{RepoServerServiceClient: rc},
   932  		commitClientset: &commitservermocks.Clientset{CommitServiceClient: cc},
   933  	}
   934  
   935  	app := newTestApp("app1")
   936  	proj := newTestProject()
   937  	projects := map[string]*v1alpha1.AppProject{app.Spec.Project: proj}
   938  
   939  	d.On("GetRepoObjs", mock.Anything, app, mock.Anything, mock.Anything, proj).Return(nil, &repoclient.ManifestResponse{Revision: "sha123"}, nil)
   940  	r.On("GetRepository", mock.Anything, mock.Anything, mock.Anything).Return(&v1alpha1.Repository{Repo: "https://example.com/repo"}, nil)
   941  	rc.On("GetRevisionMetadata", mock.Anything, mock.Anything).Return(&v1alpha1.RevisionMetadata{}, nil)
   942  	d.On("GetWriteCredentials", mock.Anything, mock.Anything, mock.Anything).Return(&v1alpha1.Repository{Repo: "https://example.com/repo"}, nil)
   943  	d.On("GetHydratorCommitMessageTemplate").Return("", errors.New("template error"))
   944  	logCtx := log.NewEntry(log.StandardLogger())
   945  
   946  	sha, hydratedSha, errs, err := h.hydrate(logCtx, []*v1alpha1.Application{app}, projects)
   947  
   948  	require.Error(t, err)
   949  	assert.Equal(t, "sha123", sha)
   950  	assert.Empty(t, hydratedSha)
   951  	assert.Empty(t, errs)
   952  	assert.ErrorContains(t, err, "template error")
   953  }
   954  
   955  func TestHydrator_hydrate_TemplatedCommitMessageError(t *testing.T) {
   956  	t.Parallel()
   957  
   958  	d := mocks.NewDependencies(t)
   959  	r := mocks.NewRepoGetter(t)
   960  	cc := commitservermocks.NewCommitServiceClient(t)
   961  	rc := reposervermocks.NewRepoServerServiceClient(t)
   962  	h := &Hydrator{
   963  		dependencies:    d,
   964  		repoGetter:      r,
   965  		repoClientset:   &reposervermocks.Clientset{RepoServerServiceClient: rc},
   966  		commitClientset: &commitservermocks.Clientset{CommitServiceClient: cc},
   967  	}
   968  
   969  	app := newTestApp("app1")
   970  	proj := newTestProject()
   971  	projects := map[string]*v1alpha1.AppProject{app.Spec.Project: proj}
   972  
   973  	d.On("GetRepoObjs", mock.Anything, app, mock.Anything, mock.Anything, proj).Return(nil, &repoclient.ManifestResponse{Revision: "sha123"}, nil)
   974  	r.On("GetRepository", mock.Anything, mock.Anything, mock.Anything).Return(&v1alpha1.Repository{Repo: "https://example.com/repo"}, nil)
   975  	rc.On("GetRevisionMetadata", mock.Anything, mock.Anything).Return(&v1alpha1.RevisionMetadata{}, nil)
   976  	d.On("GetWriteCredentials", mock.Anything, mock.Anything, mock.Anything).Return(&v1alpha1.Repository{Repo: "https://example.com/repo"}, nil)
   977  	d.On("GetHydratorCommitMessageTemplate").Return("{{ notAFunction }} template", nil)
   978  	logCtx := log.NewEntry(log.StandardLogger())
   979  
   980  	sha, hydratedSha, errs, err := h.hydrate(logCtx, []*v1alpha1.Application{app}, projects)
   981  
   982  	require.Error(t, err)
   983  	assert.Equal(t, "sha123", sha)
   984  	assert.Empty(t, hydratedSha)
   985  	assert.Empty(t, errs)
   986  	assert.ErrorContains(t, err, "failed to parse template")
   987  }
   988  
   989  func TestHydrator_hydrate_CommitHydratedManifestsError(t *testing.T) {
   990  	t.Parallel()
   991  
   992  	d := mocks.NewDependencies(t)
   993  	r := mocks.NewRepoGetter(t)
   994  	cc := commitservermocks.NewCommitServiceClient(t)
   995  	rc := reposervermocks.NewRepoServerServiceClient(t)
   996  	h := &Hydrator{
   997  		dependencies:    d,
   998  		repoGetter:      r,
   999  		repoClientset:   &reposervermocks.Clientset{RepoServerServiceClient: rc},
  1000  		commitClientset: &commitservermocks.Clientset{CommitServiceClient: cc},
  1001  	}
  1002  
  1003  	app := newTestApp("app1")
  1004  	proj := newTestProject()
  1005  	projects := map[string]*v1alpha1.AppProject{app.Spec.Project: proj}
  1006  
  1007  	d.On("GetRepoObjs", mock.Anything, app, mock.Anything, mock.Anything, proj).Return(nil, &repoclient.ManifestResponse{Revision: "sha123"}, nil)
  1008  	r.On("GetRepository", mock.Anything, mock.Anything, mock.Anything).Return(&v1alpha1.Repository{Repo: "https://example.com/repo"}, nil)
  1009  	rc.On("GetRevisionMetadata", mock.Anything, mock.Anything).Return(&v1alpha1.RevisionMetadata{}, nil)
  1010  	d.On("GetWriteCredentials", mock.Anything, mock.Anything, mock.Anything).Return(&v1alpha1.Repository{Repo: "https://example.com/repo"}, nil)
  1011  	d.On("GetHydratorCommitMessageTemplate").Return("commit message", nil)
  1012  	cc.On("CommitHydratedManifests", mock.Anything, mock.Anything).Return(nil, errors.New("commit error"))
  1013  	logCtx := log.NewEntry(log.StandardLogger())
  1014  
  1015  	sha, hydratedSha, errs, err := h.hydrate(logCtx, []*v1alpha1.Application{app}, projects)
  1016  
  1017  	require.Error(t, err)
  1018  	assert.Equal(t, "sha123", sha)
  1019  	assert.Empty(t, hydratedSha)
  1020  	assert.Empty(t, errs)
  1021  	assert.ErrorContains(t, err, "commit error")
  1022  }
  1023  
  1024  func TestHydrator_hydrate_EmptyApps(t *testing.T) {
  1025  	t.Parallel()
  1026  	d := mocks.NewDependencies(t)
  1027  	logCtx := log.NewEntry(log.StandardLogger())
  1028  	h := &Hydrator{dependencies: d}
  1029  
  1030  	sha, hydratedSha, errs, err := h.hydrate(logCtx, []*v1alpha1.Application{}, nil)
  1031  
  1032  	require.NoError(t, err)
  1033  	assert.Empty(t, sha)
  1034  	assert.Empty(t, hydratedSha)
  1035  	assert.Empty(t, errs)
  1036  }
  1037  
  1038  func TestHydrator_getManifests_Success(t *testing.T) {
  1039  	t.Parallel()
  1040  	d := mocks.NewDependencies(t)
  1041  	h := &Hydrator{dependencies: d}
  1042  	app := newTestApp("test-app")
  1043  	proj := newTestProject()
  1044  
  1045  	cm := kube.MustToUnstructured(&corev1.ConfigMap{
  1046  		ObjectMeta: metav1.ObjectMeta{
  1047  			Name: "test",
  1048  		},
  1049  	})
  1050  
  1051  	d.On("GetRepoObjs", mock.Anything, app, app.Spec.SourceHydrator.GetDrySource(), "sha123", proj).Return([]*unstructured.Unstructured{cm}, &repoclient.ManifestResponse{
  1052  		Revision: "sha123",
  1053  		Commands: []string{"cmd1", "cmd2"},
  1054  	}, nil)
  1055  
  1056  	rev, pathDetails, err := h.getManifests(context.Background(), app, "sha123", proj)
  1057  	require.NoError(t, err)
  1058  	assert.Equal(t, "sha123", rev)
  1059  	assert.Equal(t, app.Spec.SourceHydrator.SyncSource.Path, pathDetails.Path)
  1060  	assert.Equal(t, []string{"cmd1", "cmd2"}, pathDetails.Commands)
  1061  	assert.Len(t, pathDetails.Manifests, 1)
  1062  	assert.JSONEq(t, `{"metadata":{"name":"test"}}`, pathDetails.Manifests[0].ManifestJSON)
  1063  }
  1064  
  1065  func TestHydrator_getManifests_EmptyTargetRevision(t *testing.T) {
  1066  	t.Parallel()
  1067  	d := mocks.NewDependencies(t)
  1068  	h := &Hydrator{dependencies: d}
  1069  	app := newTestApp("test-app")
  1070  	proj := newTestProject()
  1071  
  1072  	d.On("GetRepoObjs", mock.Anything, app, mock.Anything, "main", proj).Return([]*unstructured.Unstructured{}, &repoclient.ManifestResponse{Revision: "sha123"}, nil)
  1073  
  1074  	rev, pathDetails, err := h.getManifests(context.Background(), app, "", proj)
  1075  	require.NoError(t, err)
  1076  	assert.Equal(t, "sha123", rev)
  1077  	assert.NotNil(t, pathDetails)
  1078  }
  1079  
  1080  func TestHydrator_getManifests_GetRepoObjsError(t *testing.T) {
  1081  	t.Parallel()
  1082  	d := mocks.NewDependencies(t)
  1083  	h := &Hydrator{dependencies: d}
  1084  	app := newTestApp("test-app")
  1085  	proj := newTestProject()
  1086  
  1087  	d.On("GetRepoObjs", mock.Anything, app, mock.Anything, "main", proj).Return(nil, nil, errors.New("repo error"))
  1088  
  1089  	rev, pathDetails, err := h.getManifests(context.Background(), app, "main", proj)
  1090  	require.Error(t, err)
  1091  	assert.Contains(t, err.Error(), "repo error")
  1092  	assert.Empty(t, rev)
  1093  	assert.Nil(t, pathDetails)
  1094  }