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  }