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 }