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