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  }