github.com/argoproj/argo-cd/v2@v2.10.9/applicationset/services/scm_provider/azure_devops_test.go (about) 1 package scm_provider 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 8 "github.com/google/uuid" 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/mock" 11 "k8s.io/utils/pointer" 12 13 azureMock "github.com/argoproj/argo-cd/v2/applicationset/services/scm_provider/azure_devops/git/mocks" 14 "github.com/microsoft/azure-devops-go-api/azuredevops" 15 azureGit "github.com/microsoft/azure-devops-go-api/azuredevops/git" 16 ) 17 18 func s(input string) *string { 19 return pointer.String(input) 20 } 21 22 func TestAzureDevopsRepoHasPath(t *testing.T) { 23 organization := "myorg" 24 teamProject := "myorg_project" 25 repoName := "myorg_project_repo" 26 path := "dir/subdir/item.yaml" 27 branchName := "my/featurebranch" 28 29 ctx := context.Background() 30 uuid := uuid.New().String() 31 32 testCases := []struct { 33 name string 34 pathFound bool 35 azureDevopsError error 36 returnError bool 37 errorMessage string 38 clientError error 39 }{ 40 { 41 name: "RepoHasPath when Azure DevOps client factory fails returns error", 42 clientError: fmt.Errorf("Client factory error"), 43 }, 44 { 45 name: "RepoHasPath when found returns true", 46 pathFound: true, 47 }, 48 { 49 name: "RepoHasPath when no path found returns false", 50 pathFound: false, 51 azureDevopsError: azuredevops.WrappedError{TypeKey: s(AzureDevOpsErrorsTypeKeyValues.GitItemNotFound)}, 52 }, 53 { 54 name: "RepoHasPath when unknown Azure DevOps WrappedError occurs returns error", 55 pathFound: false, 56 azureDevopsError: azuredevops.WrappedError{TypeKey: s("OtherAzureDevopsException")}, 57 returnError: true, 58 errorMessage: "failed to check for path existence", 59 }, 60 { 61 name: "RepoHasPath when unknown Azure DevOps error occurs returns error", 62 pathFound: false, 63 azureDevopsError: fmt.Errorf("Undefined error from Azure Devops"), 64 returnError: true, 65 errorMessage: "failed to check for path existence", 66 }, 67 { 68 name: "RepoHasPath when wrapped Azure DevOps error occurs without TypeKey returns error", 69 pathFound: false, 70 azureDevopsError: azuredevops.WrappedError{}, 71 returnError: true, 72 errorMessage: "failed to check for path existence", 73 }, 74 } 75 76 for _, testCase := range testCases { 77 t.Run(testCase.name, func(t *testing.T) { 78 gitClientMock := azureMock.Client{} 79 80 clientFactoryMock := &AzureClientFactoryMock{mock: &mock.Mock{}} 81 clientFactoryMock.mock.On("GetClient", mock.Anything).Return(&gitClientMock, testCase.clientError) 82 83 repoId := &uuid 84 gitClientMock.On("GetItem", ctx, azureGit.GetItemArgs{Project: &teamProject, Path: &path, VersionDescriptor: &azureGit.GitVersionDescriptor{Version: &branchName}, RepositoryId: repoId}).Return(nil, testCase.azureDevopsError) 85 86 provider := AzureDevOpsProvider{organization: organization, teamProject: teamProject, clientFactory: clientFactoryMock} 87 88 repo := &Repository{Organization: organization, Repository: repoName, RepositoryId: uuid, Branch: branchName} 89 hasPath, err := provider.RepoHasPath(ctx, repo, path) 90 91 if testCase.clientError != nil { 92 assert.ErrorContains(t, err, testCase.clientError.Error()) 93 gitClientMock.AssertNotCalled(t, "GetItem", ctx, azureGit.GetItemArgs{Project: &teamProject, Path: &path, VersionDescriptor: &azureGit.GitVersionDescriptor{Version: &branchName}, RepositoryId: repoId}) 94 95 return 96 } 97 98 if testCase.returnError { 99 assert.ErrorContains(t, err, testCase.errorMessage) 100 } 101 102 assert.Equal(t, testCase.pathFound, hasPath) 103 104 gitClientMock.AssertCalled(t, "GetItem", ctx, azureGit.GetItemArgs{Project: &teamProject, Path: &path, VersionDescriptor: &azureGit.GitVersionDescriptor{Version: &branchName}, RepositoryId: repoId}) 105 106 }) 107 } 108 } 109 110 func TestGetDefaultBranchOnDisabledRepo(t *testing.T) { 111 112 organization := "myorg" 113 teamProject := "myorg_project" 114 repoName := "myorg_project_repo" 115 defaultBranch := "main" 116 117 ctx := context.Background() 118 119 testCases := []struct { 120 name string 121 azureDevOpsError error 122 shouldReturnError bool 123 }{ 124 { 125 name: "azure devops error when disabled repo causes empty return value", 126 azureDevOpsError: azuredevops.WrappedError{TypeKey: s(AzureDevOpsErrorsTypeKeyValues.GitRepositoryNotFound)}, 127 shouldReturnError: false, 128 }, 129 { 130 name: "azure devops error with unknown error type returns error", 131 azureDevOpsError: azuredevops.WrappedError{TypeKey: s("OtherError")}, 132 shouldReturnError: true, 133 }, 134 { 135 name: "other error when calling azure devops returns error", 136 azureDevOpsError: fmt.Errorf("some unknown error"), 137 shouldReturnError: true, 138 }, 139 } 140 141 for _, testCase := range testCases { 142 t.Run(testCase.name, func(t *testing.T) { 143 uuid := uuid.New().String() 144 145 gitClientMock := azureMock.Client{} 146 147 clientFactoryMock := &AzureClientFactoryMock{mock: &mock.Mock{}} 148 clientFactoryMock.mock.On("GetClient", mock.Anything).Return(&gitClientMock, nil) 149 150 gitClientMock.On("GetBranch", ctx, azureGit.GetBranchArgs{RepositoryId: &repoName, Project: &teamProject, Name: &defaultBranch}).Return(nil, testCase.azureDevOpsError) 151 152 repo := &Repository{Organization: organization, Repository: repoName, RepositoryId: uuid, Branch: defaultBranch} 153 154 provider := AzureDevOpsProvider{organization: organization, teamProject: teamProject, clientFactory: clientFactoryMock, allBranches: false} 155 branches, err := provider.GetBranches(ctx, repo) 156 157 if testCase.shouldReturnError { 158 assert.Error(t, err) 159 } else { 160 assert.NoError(t, err) 161 } 162 163 assert.Empty(t, branches) 164 165 gitClientMock.AssertExpectations(t) 166 }) 167 } 168 } 169 170 func TestGetAllBranchesOnDisabledRepo(t *testing.T) { 171 172 organization := "myorg" 173 teamProject := "myorg_project" 174 repoName := "myorg_project_repo" 175 defaultBranch := "main" 176 177 ctx := context.Background() 178 179 testCases := []struct { 180 name string 181 azureDevOpsError error 182 shouldReturnError bool 183 }{ 184 { 185 name: "azure devops error when disabled repo causes empty return value", 186 azureDevOpsError: azuredevops.WrappedError{TypeKey: s(AzureDevOpsErrorsTypeKeyValues.GitRepositoryNotFound)}, 187 shouldReturnError: false, 188 }, 189 { 190 name: "azure devops error with unknown error type returns error", 191 azureDevOpsError: azuredevops.WrappedError{TypeKey: s("OtherError")}, 192 shouldReturnError: true, 193 }, 194 { 195 name: "other error when calling azure devops returns error", 196 azureDevOpsError: fmt.Errorf("some unknown error"), 197 shouldReturnError: true, 198 }, 199 } 200 201 for _, testCase := range testCases { 202 t.Run(testCase.name, func(t *testing.T) { 203 uuid := uuid.New().String() 204 205 gitClientMock := azureMock.Client{} 206 207 clientFactoryMock := &AzureClientFactoryMock{mock: &mock.Mock{}} 208 clientFactoryMock.mock.On("GetClient", mock.Anything).Return(&gitClientMock, nil) 209 210 gitClientMock.On("GetBranches", ctx, azureGit.GetBranchesArgs{RepositoryId: &repoName, Project: &teamProject}).Return(nil, testCase.azureDevOpsError) 211 212 repo := &Repository{Organization: organization, Repository: repoName, RepositoryId: uuid, Branch: defaultBranch} 213 214 provider := AzureDevOpsProvider{organization: organization, teamProject: teamProject, clientFactory: clientFactoryMock, allBranches: true} 215 branches, err := provider.GetBranches(ctx, repo) 216 217 if testCase.shouldReturnError { 218 assert.Error(t, err) 219 } else { 220 assert.NoError(t, err) 221 } 222 223 assert.Empty(t, branches) 224 225 gitClientMock.AssertExpectations(t) 226 }) 227 } 228 } 229 230 func TestAzureDevOpsGetDefaultBranchStripsRefsName(t *testing.T) { 231 232 t.Run("Get branches only default branch removes characters before querying azure devops", func(t *testing.T) { 233 234 organization := "myorg" 235 teamProject := "myorg_project" 236 repoName := "myorg_project_repo" 237 238 ctx := context.Background() 239 uuid := uuid.New().String() 240 strippedBranchName := "somebranch" 241 defaultBranch := fmt.Sprintf("refs/heads/%v", strippedBranchName) 242 243 branchReturn := &azureGit.GitBranchStats{Name: &strippedBranchName, Commit: &azureGit.GitCommitRef{CommitId: s("abc123233223")}} 244 repo := &Repository{Organization: organization, Repository: repoName, RepositoryId: uuid, Branch: defaultBranch} 245 246 gitClientMock := azureMock.Client{} 247 248 clientFactoryMock := &AzureClientFactoryMock{mock: &mock.Mock{}} 249 clientFactoryMock.mock.On("GetClient", mock.Anything).Return(&gitClientMock, nil) 250 251 gitClientMock.On("GetBranch", ctx, azureGit.GetBranchArgs{RepositoryId: &repoName, Project: &teamProject, Name: &strippedBranchName}).Return(branchReturn, nil) 252 253 provider := AzureDevOpsProvider{organization: organization, teamProject: teamProject, clientFactory: clientFactoryMock, allBranches: false} 254 branches, err := provider.GetBranches(ctx, repo) 255 256 assert.NoError(t, err) 257 assert.Len(t, branches, 1) 258 assert.Equal(t, strippedBranchName, branches[0].Branch) 259 260 gitClientMock.AssertCalled(t, "GetBranch", ctx, azureGit.GetBranchArgs{RepositoryId: &repoName, Project: &teamProject, Name: &strippedBranchName}) 261 }) 262 } 263 264 func TestAzureDevOpsGetBranchesDefultBranchOnly(t *testing.T) { 265 organization := "myorg" 266 teamProject := "myorg_project" 267 repoName := "myorg_project_repo" 268 269 ctx := context.Background() 270 uuid := uuid.New().String() 271 272 defaultBranch := "main" 273 274 testCases := []struct { 275 name string 276 expectedBranch *azureGit.GitBranchStats 277 getBranchesApiError error 278 clientError error 279 }{ 280 { 281 name: "GetBranches AllBranches false when single branch returned returns branch", 282 expectedBranch: &azureGit.GitBranchStats{Name: &defaultBranch, Commit: &azureGit.GitCommitRef{CommitId: s("abc123233223")}}, 283 }, 284 { 285 name: "GetBranches AllBranches false when request fails returns error and empty result", 286 getBranchesApiError: fmt.Errorf("Remote Azure Devops GetBranches error"), 287 }, 288 { 289 name: "GetBranches AllBranches false when Azure DevOps client fails returns error", 290 clientError: fmt.Errorf("Could not get Azure Devops API client"), 291 }, 292 { 293 name: "GetBranches AllBranches false when branch returned with long commit SHA", 294 expectedBranch: &azureGit.GitBranchStats{Name: &defaultBranch, Commit: &azureGit.GitCommitRef{CommitId: s("53863052ADF24229AB72154B4D83DAB7")}}, 295 }, 296 } 297 298 for _, testCase := range testCases { 299 t.Run(testCase.name, func(t *testing.T) { 300 gitClientMock := azureMock.Client{} 301 302 clientFactoryMock := &AzureClientFactoryMock{mock: &mock.Mock{}} 303 clientFactoryMock.mock.On("GetClient", mock.Anything).Return(&gitClientMock, testCase.clientError) 304 305 gitClientMock.On("GetBranch", ctx, azureGit.GetBranchArgs{RepositoryId: &repoName, Project: &teamProject, Name: &defaultBranch}).Return(testCase.expectedBranch, testCase.getBranchesApiError) 306 307 repo := &Repository{Organization: organization, Repository: repoName, RepositoryId: uuid, Branch: defaultBranch} 308 309 provider := AzureDevOpsProvider{organization: organization, teamProject: teamProject, clientFactory: clientFactoryMock, allBranches: false} 310 branches, err := provider.GetBranches(ctx, repo) 311 312 if testCase.clientError != nil { 313 assert.ErrorContains(t, err, testCase.clientError.Error()) 314 gitClientMock.AssertNotCalled(t, "GetBranch", ctx, azureGit.GetBranchArgs{RepositoryId: &repoName, Project: &teamProject, Name: &defaultBranch}) 315 316 return 317 } 318 319 if testCase.getBranchesApiError != nil { 320 assert.Empty(t, branches) 321 assert.ErrorContains(t, err, testCase.getBranchesApiError.Error()) 322 } else { 323 if testCase.expectedBranch != nil { 324 assert.NotEmpty(t, branches) 325 } 326 assert.Len(t, branches, 1) 327 assert.Equal(t, repo.RepositoryId, branches[0].RepositoryId) 328 } 329 330 gitClientMock.AssertCalled(t, "GetBranch", ctx, azureGit.GetBranchArgs{RepositoryId: &repoName, Project: &teamProject, Name: &defaultBranch}) 331 }) 332 } 333 } 334 335 func TestAzureDevopsGetBranches(t *testing.T) { 336 organization := "myorg" 337 teamProject := "myorg_project" 338 repoName := "myorg_project_repo" 339 340 ctx := context.Background() 341 uuid := uuid.New().String() 342 343 testCases := []struct { 344 name string 345 expectedBranches *[]azureGit.GitBranchStats 346 getBranchesApiError error 347 clientError error 348 allBranches bool 349 expectedProcessingErrorMsg string 350 }{ 351 { 352 name: "GetBranches when single branch returned returns this branch info", 353 expectedBranches: &[]azureGit.GitBranchStats{{Name: s("feature-feat1"), Commit: &azureGit.GitCommitRef{CommitId: s("abc123233223")}}}, 354 allBranches: true, 355 }, 356 { 357 name: "GetBranches when Azure DevOps request fails returns error and empty result", 358 getBranchesApiError: fmt.Errorf("Remote Azure Devops GetBranches error"), 359 allBranches: true, 360 }, 361 { 362 name: "GetBranches when no branches returned returns error", 363 allBranches: true, 364 expectedProcessingErrorMsg: "empty branch result", 365 }, 366 { 367 name: "GetBranches when git client retrievel fails returns error", 368 clientError: fmt.Errorf("Could not get Azure Devops API client"), 369 allBranches: true, 370 }, 371 { 372 name: "GetBranches when multiple branches returned returns branch info for all branches", 373 expectedBranches: &[]azureGit.GitBranchStats{ 374 {Name: s("feature-feat1"), Commit: &azureGit.GitCommitRef{CommitId: s("abc123233223")}}, 375 {Name: s("feature/feat2"), Commit: &azureGit.GitCommitRef{CommitId: s("4334")}}, 376 {Name: s("feature/feat2"), Commit: &azureGit.GitCommitRef{CommitId: s("53863052ADF24229AB72154B4D83DAB7")}}, 377 }, 378 allBranches: true, 379 }, 380 } 381 382 for _, testCase := range testCases { 383 t.Run(testCase.name, func(t *testing.T) { 384 gitClientMock := azureMock.Client{} 385 386 clientFactoryMock := &AzureClientFactoryMock{mock: &mock.Mock{}} 387 clientFactoryMock.mock.On("GetClient", mock.Anything).Return(&gitClientMock, testCase.clientError) 388 389 gitClientMock.On("GetBranches", ctx, azureGit.GetBranchesArgs{RepositoryId: &repoName, Project: &teamProject}).Return(testCase.expectedBranches, testCase.getBranchesApiError) 390 391 repo := &Repository{Organization: organization, Repository: repoName, RepositoryId: uuid} 392 393 provider := AzureDevOpsProvider{organization: organization, teamProject: teamProject, clientFactory: clientFactoryMock, allBranches: testCase.allBranches} 394 branches, err := provider.GetBranches(ctx, repo) 395 396 if testCase.expectedProcessingErrorMsg != "" { 397 assert.ErrorContains(t, err, testCase.expectedProcessingErrorMsg) 398 assert.Nil(t, branches) 399 400 return 401 } 402 if testCase.clientError != nil { 403 assert.ErrorContains(t, err, testCase.clientError.Error()) 404 gitClientMock.AssertNotCalled(t, "GetBranches", ctx, azureGit.GetBranchesArgs{RepositoryId: &repoName, Project: &teamProject}) 405 return 406 407 } 408 409 if testCase.getBranchesApiError != nil { 410 assert.Empty(t, branches) 411 assert.ErrorContains(t, err, testCase.getBranchesApiError.Error()) 412 } else { 413 if len(*testCase.expectedBranches) > 0 { 414 assert.NotEmpty(t, branches) 415 } 416 assert.Len(t, branches, len(*testCase.expectedBranches)) 417 for _, branch := range branches { 418 assert.NotEmpty(t, branch.RepositoryId) 419 assert.Equal(t, repo.RepositoryId, branch.RepositoryId) 420 } 421 } 422 423 gitClientMock.AssertCalled(t, "GetBranches", ctx, azureGit.GetBranchesArgs{RepositoryId: &repoName, Project: &teamProject}) 424 }) 425 } 426 } 427 428 func TestGetAzureDevopsRepositories(t *testing.T) { 429 organization := "myorg" 430 teamProject := "myorg_project" 431 432 uuid := uuid.New() 433 ctx := context.Background() 434 435 repoId := &uuid 436 437 testCases := []struct { 438 name string 439 getRepositoriesError error 440 repositories []azureGit.GitRepository 441 expectedNumberOfRepos int 442 }{ 443 { 444 name: "ListRepos when single repo found returns repo info", 445 repositories: []azureGit.GitRepository{{Name: s("repo1"), DefaultBranch: s("main"), RemoteUrl: s("https://remoteurl.u"), Id: repoId}}, 446 expectedNumberOfRepos: 1, 447 }, 448 { 449 name: "ListRepos when repo has no default branch returns empty list", 450 repositories: []azureGit.GitRepository{{Name: s("repo2"), RemoteUrl: s("https://remoteurl.u"), Id: repoId}}, 451 }, 452 { 453 name: "ListRepos when Azure DevOps request fails returns error", 454 getRepositoriesError: fmt.Errorf("Could not get repos"), 455 }, 456 { 457 name: "ListRepos when repo has no name returns empty list", 458 repositories: []azureGit.GitRepository{{DefaultBranch: s("main"), RemoteUrl: s("https://remoteurl.u"), Id: repoId}}, 459 }, 460 { 461 name: "ListRepos when repo has no remote URL returns empty list", 462 repositories: []azureGit.GitRepository{{DefaultBranch: s("main"), Name: s("repo_name"), Id: repoId}}, 463 }, 464 { 465 name: "ListRepos when repo has no ID returns empty list", 466 repositories: []azureGit.GitRepository{{DefaultBranch: s("main"), Name: s("repo_name"), RemoteUrl: s("https://remoteurl.u")}}, 467 }, 468 { 469 name: "ListRepos when multiple repos returned returns list of eligible repos only", 470 repositories: []azureGit.GitRepository{ 471 {Name: s("returned1"), DefaultBranch: s("main"), RemoteUrl: s("https://remoteurl.u"), Id: repoId}, 472 {Name: s("missing_default_branch"), RemoteUrl: s("https://remoteurl.u"), Id: repoId}, 473 {DefaultBranch: s("missing_name"), RemoteUrl: s("https://remoteurl.u"), Id: repoId}, 474 {Name: s("missing_remote_url"), DefaultBranch: s("main"), Id: repoId}, 475 {Name: s("missing_id"), DefaultBranch: s("main"), RemoteUrl: s("https://remoteurl.u")}}, 476 expectedNumberOfRepos: 1, 477 }, 478 } 479 480 for _, testCase := range testCases { 481 t.Run(testCase.name, func(t *testing.T) { 482 483 gitClientMock := azureMock.Client{} 484 gitClientMock.On("GetRepositories", ctx, azureGit.GetRepositoriesArgs{Project: s(teamProject)}).Return(&testCase.repositories, testCase.getRepositoriesError) 485 486 clientFactoryMock := &AzureClientFactoryMock{mock: &mock.Mock{}} 487 clientFactoryMock.mock.On("GetClient", mock.Anything).Return(&gitClientMock) 488 489 provider := AzureDevOpsProvider{organization: organization, teamProject: teamProject, clientFactory: clientFactoryMock} 490 491 repositories, err := provider.ListRepos(ctx, "https") 492 493 if testCase.getRepositoriesError != nil { 494 assert.Error(t, err, "Expected an error from test case %v", testCase.name) 495 } 496 497 if testCase.expectedNumberOfRepos == 0 { 498 assert.Empty(t, repositories) 499 } else { 500 assert.NotEmpty(t, repositories) 501 assert.Len(t, repositories, testCase.expectedNumberOfRepos) 502 } 503 504 gitClientMock.AssertExpectations(t) 505 }) 506 } 507 } 508 509 type AzureClientFactoryMock struct { 510 mock *mock.Mock 511 } 512 513 func (m *AzureClientFactoryMock) GetClient(ctx context.Context) (azureGit.Client, error) { 514 args := m.mock.Called(ctx) 515 516 var client azureGit.Client 517 c := args.Get(0) 518 if c != nil { 519 client = c.(azureGit.Client) 520 } 521 522 var err error 523 if len(args) > 1 { 524 if e, ok := args.Get(1).(error); ok { 525 err = e 526 } 527 } 528 529 return client, err 530 }