github.com/xgoffin/jenkins-library@v1.154.0/cmd/sonarExecuteScan_test.go (about)

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"net/http"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"testing"
    11  
    12  	"github.com/bmatcuk/doublestar"
    13  	"github.com/jarcoal/httpmock"
    14  	"github.com/pkg/errors"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  
    18  	piperHttp "github.com/SAP/jenkins-library/pkg/http"
    19  	"github.com/SAP/jenkins-library/pkg/mock"
    20  	FileUtils "github.com/SAP/jenkins-library/pkg/piperutils"
    21  	SonarUtils "github.com/SAP/jenkins-library/pkg/sonar"
    22  )
    23  
    24  //TODO: extract to mock package
    25  type mockDownloader struct {
    26  	shouldFail    bool
    27  	requestedURL  []string
    28  	requestedFile []string
    29  }
    30  
    31  func (m *mockDownloader) DownloadFile(url, filename string, header http.Header, cookies []*http.Cookie) error {
    32  	m.requestedURL = append(m.requestedURL, url)
    33  	m.requestedFile = append(m.requestedFile, filename)
    34  	if m.shouldFail {
    35  		return errors.New("something happened")
    36  	}
    37  	return nil
    38  }
    39  
    40  func (m *mockDownloader) SetOptions(options piperHttp.ClientOptions) {}
    41  
    42  func mockFileUtilsExists(exists bool) func(string) (bool, error) {
    43  	return func(filename string) (bool, error) {
    44  		if exists {
    45  			return true, nil
    46  		}
    47  		return false, errors.New("something happened")
    48  	}
    49  }
    50  
    51  func mockExecLookPath(executable string) (string, error) {
    52  	if executable == "local-sonar-scanner" {
    53  		return "/usr/bin/sonar-scanner", nil
    54  	}
    55  	return "", errors.New("something happened")
    56  }
    57  
    58  func mockFileUtilsUnzip(t *testing.T, expectSrc string) func(string, string) ([]string, error) {
    59  	return func(src, dest string) ([]string, error) {
    60  		assert.Equal(t, filepath.Join(dest, expectSrc), src)
    61  		return []string{}, nil
    62  	}
    63  }
    64  
    65  func mockOsRename(t *testing.T, expectOld, expectNew string) func(string, string) error {
    66  	return func(old, new string) error {
    67  		assert.Regexp(t, expectOld, old)
    68  		assert.Equal(t, expectNew, new)
    69  		return nil
    70  	}
    71  }
    72  
    73  func mockOsStat(exists map[string]bool) func(name string) (os.FileInfo, error) {
    74  	return func(name string) (os.FileInfo, error) {
    75  		_, exists := exists[name]
    76  		if exists {
    77  			// Exploits the fact that FileInfo result from os.Stat() is ignored anyway
    78  			return nil, nil
    79  		}
    80  		return nil, errors.New("something happened")
    81  	}
    82  }
    83  
    84  func mockGlob(matchesForPatterns map[string][]string) func(pattern string) ([]string, error) {
    85  	return func(pattern string) ([]string, error) {
    86  		matches, exists := matchesForPatterns[pattern]
    87  		if exists {
    88  			return matches, nil
    89  		}
    90  		return nil, errors.New("something happened")
    91  	}
    92  }
    93  
    94  func createTaskReportFile(t *testing.T, workingDir string) {
    95  	require.NoError(t, os.MkdirAll(filepath.Join(workingDir, ".scannerwork"), 0755))
    96  	require.NoError(t, ioutil.WriteFile(filepath.Join(workingDir, ".scannerwork", "report-task.txt"), []byte(taskReportContent), 0755))
    97  	require.FileExists(t, filepath.Join(workingDir, ".scannerwork", "report-task.txt"))
    98  }
    99  
   100  const sonarServerURL = "https://sonarcloud.io"
   101  
   102  const taskReportContent = `
   103  projectKey=piper-test
   104  serverUrl=` + sonarServerURL + `
   105  serverVersion=8.0.0.12345
   106  dashboardUrl=` + sonarServerURL + `/dashboard/index/piper-test
   107  ceTaskId=AXERR2JBbm9IiM5TEST
   108  ceTaskUrl=` + sonarServerURL + `/api/ce/task?id=AXERR2JBbm9IiMTEST
   109  `
   110  
   111  const measuresComponentResponse = `
   112  {
   113  	"component": {
   114  	  "key": "com.sap.piper.test",
   115  	  "name": "com.sap.piper.test",
   116  	  "qualifier": "TRK",
   117  	  "measures": [
   118  		{
   119  		  "metric": "line_coverage",
   120  		  "value": "80.4",
   121  		  "bestValue": false
   122  		},
   123  		{
   124  		  "metric": "branch_coverage",
   125  		  "value": "81.0",
   126  		  "bestValue": false
   127  		},
   128  		{
   129  		  "metric": "coverage",
   130  		  "value": "80.7",
   131  		  "bestValue": false
   132  		},
   133  		{
   134  		  "metric": "extra_valie",
   135  		  "value": "42.7",
   136  		  "bestValue": false
   137  		}
   138  	  ]
   139  	}
   140    }
   141  `
   142  
   143  func TestRunSonar(t *testing.T) {
   144  	mockRunner := mock.ExecMockRunner{}
   145  	mockDownloadClient := mockDownloader{shouldFail: false}
   146  	apiClient := &piperHttp.Client{}
   147  	apiClient.SetOptions(piperHttp.ClientOptions{MaxRetries: -1, UseDefaultTransport: true})
   148  	// mock SonarQube API calls
   149  	httpmock.Activate()
   150  	defer httpmock.DeactivateAndReset()
   151  	// add response handler
   152  	httpmock.RegisterResponder(http.MethodGet, sonarServerURL+"/api/"+SonarUtils.EndpointCeTask+"", httpmock.NewStringResponder(http.StatusOK, `{ "task": { "componentId": "AXERR2JBbm9IiM5TEST", "status": "SUCCESS" }}`))
   153  	httpmock.RegisterResponder(http.MethodGet, sonarServerURL+"/api/"+SonarUtils.EndpointIssuesSearch+"", httpmock.NewStringResponder(http.StatusOK, `{ "total": 0 }`))
   154  	httpmock.RegisterResponder(http.MethodGet, sonarServerURL+"/api/"+SonarUtils.EndpointMeasuresComponent+"", httpmock.NewStringResponder(http.StatusOK, measuresComponentResponse))
   155  
   156  	t.Run("default", func(t *testing.T) {
   157  		// init
   158  		tmpFolder, err := ioutil.TempDir(".", "test-sonar-")
   159  		require.NoError(t, err)
   160  		defer os.RemoveAll(tmpFolder)
   161  		createTaskReportFile(t, tmpFolder)
   162  
   163  		sonar = sonarSettings{
   164  			workingDir:  tmpFolder,
   165  			binary:      "sonar-scanner",
   166  			environment: []string{},
   167  			options:     []string{},
   168  		}
   169  		options := sonarExecuteScanOptions{
   170  			CustomTLSCertificateLinks: []string{},
   171  			Token:                     "secret-ABC",
   172  			ServerURL:                 sonarServerURL,
   173  			Organization:              "SAP",
   174  			Version:                   "1.2.3",
   175  			VersioningModel:           "major",
   176  			PullRequestProvider:       "GitHub",
   177  		}
   178  		fileUtilsExists = mockFileUtilsExists(true)
   179  		// test
   180  		err = runSonar(options, &mockDownloadClient, &mockRunner, apiClient, &sonarExecuteScanInflux{})
   181  		// assert
   182  		assert.NoError(t, err)
   183  		assert.Contains(t, sonar.options, "-Dsonar.projectVersion=1")
   184  		assert.Contains(t, sonar.options, "-Dsonar.organization=SAP")
   185  		assert.Contains(t, sonar.environment, "SONAR_HOST_URL="+sonarServerURL)
   186  		assert.Contains(t, sonar.environment, "SONAR_TOKEN=secret-ABC")
   187  		assert.Contains(t, sonar.environment, "SONAR_SCANNER_OPTS=-Djavax.net.ssl.trustStore="+filepath.Join(getWorkingDir(), ".certificates", "cacerts")+" -Djavax.net.ssl.trustStorePassword=changeit")
   188  		assert.FileExists(t, filepath.Join(sonar.workingDir, "sonarExecuteScan_reports.json"))
   189  		assert.FileExists(t, filepath.Join(sonar.workingDir, "sonarExecuteScan_links.json"))
   190  	})
   191  	t.Run("with custom options", func(t *testing.T) {
   192  		// init
   193  		tmpFolder, err := ioutil.TempDir(".", "test-sonar-")
   194  		require.NoError(t, err)
   195  		defer os.RemoveAll(tmpFolder)
   196  		createTaskReportFile(t, tmpFolder)
   197  
   198  		sonar = sonarSettings{
   199  			workingDir:  tmpFolder,
   200  			binary:      "sonar-scanner",
   201  			environment: []string{},
   202  			options:     []string{},
   203  		}
   204  		options := sonarExecuteScanOptions{
   205  			Options:             []string{"-Dsonar.projectKey=piper"},
   206  			PullRequestProvider: "GitHub",
   207  		}
   208  		fileUtilsExists = mockFileUtilsExists(true)
   209  		defer func() {
   210  			fileUtilsExists = FileUtils.FileExists
   211  		}()
   212  		// test
   213  		err = runSonar(options, &mockDownloadClient, &mockRunner, apiClient, &sonarExecuteScanInflux{})
   214  		// assert
   215  		assert.NoError(t, err)
   216  		assert.Contains(t, sonar.options, "-Dsonar.projectKey=piper")
   217  	})
   218  	t.Run("with binaries option", func(t *testing.T) {
   219  		// init
   220  		tmpFolder, err := ioutil.TempDir(".", "test-sonar-")
   221  		require.NoError(t, err)
   222  		defer func() { _ = os.RemoveAll(tmpFolder) }()
   223  		createTaskReportFile(t, tmpFolder)
   224  
   225  		sonar = sonarSettings{
   226  			workingDir:  tmpFolder,
   227  			binary:      "sonar-scanner",
   228  			environment: []string{},
   229  			options:     []string{},
   230  		}
   231  		fileUtilsExists = mockFileUtilsExists(true)
   232  
   233  		globMatches := make(map[string][]string)
   234  		globMatches[pomXMLPattern] = []string{"pom.xml", "application/pom.xml"}
   235  		doublestarGlob = mockGlob(globMatches)
   236  
   237  		existsMap := make(map[string]bool)
   238  		existsMap[filepath.Join("target", "classes")] = true
   239  		existsMap[filepath.Join("target", "test-classes")] = true
   240  		existsMap[filepath.Join("application", "target", "classes")] = true
   241  		osStat = mockOsStat(existsMap)
   242  
   243  		defer func() {
   244  			fileUtilsExists = FileUtils.FileExists
   245  			doublestarGlob = doublestar.Glob
   246  			osStat = os.Stat
   247  		}()
   248  		options := sonarExecuteScanOptions{
   249  			InferJavaBinaries:   true,
   250  			PullRequestProvider: "GitHub",
   251  		}
   252  		// test
   253  		err = runSonar(options, &mockDownloadClient, &mockRunner, apiClient, &sonarExecuteScanInflux{})
   254  		// assert
   255  		assert.NoError(t, err)
   256  		assert.Contains(t, sonar.options, fmt.Sprintf("-Dsonar.java.binaries=%s,%s,%s",
   257  			filepath.Join("target", "classes"),
   258  			filepath.Join("target", "test-classes"),
   259  			filepath.Join("application", "target", "classes")))
   260  	})
   261  	t.Run("with binaries option already given", func(t *testing.T) {
   262  		// init
   263  		tmpFolder, err := ioutil.TempDir(".", "test-sonar-")
   264  		require.NoError(t, err)
   265  		defer func() { _ = os.RemoveAll(tmpFolder) }()
   266  		createTaskReportFile(t, tmpFolder)
   267  
   268  		sonar = sonarSettings{
   269  			workingDir:  tmpFolder,
   270  			binary:      "sonar-scanner",
   271  			environment: []string{},
   272  			options:     []string{},
   273  		}
   274  		fileUtilsExists = mockFileUtilsExists(true)
   275  
   276  		globMatches := make(map[string][]string)
   277  		globMatches[pomXMLPattern] = []string{"pom.xml"}
   278  		doublestarGlob = mockGlob(globMatches)
   279  
   280  		existsMap := make(map[string]bool)
   281  		existsMap[filepath.Join("target", "classes")] = true
   282  		osStat = mockOsStat(existsMap)
   283  
   284  		defer func() {
   285  			fileUtilsExists = FileUtils.FileExists
   286  			doublestarGlob = doublestar.Glob
   287  			osStat = os.Stat
   288  		}()
   289  		options := sonarExecuteScanOptions{
   290  			Options:             []string{"-Dsonar.java.binaries=user/provided"},
   291  			InferJavaBinaries:   true,
   292  			PullRequestProvider: "GitHub",
   293  		}
   294  		// test
   295  		err = runSonar(options, &mockDownloadClient, &mockRunner, apiClient, &sonarExecuteScanInflux{})
   296  		// assert
   297  		assert.NoError(t, err)
   298  		assert.NotContains(t, sonar.options, fmt.Sprintf("-Dsonar.java.binaries=%s",
   299  			filepath.Join("target", "classes")))
   300  		assert.Contains(t, sonar.options, "-Dsonar.java.binaries=user/provided")
   301  	})
   302  	t.Run("projectKey, coverageExclusions, m2Path, verbose", func(t *testing.T) {
   303  		// init
   304  		tmpFolder, err := ioutil.TempDir(".", "test-sonar-")
   305  		require.NoError(t, err)
   306  		defer os.RemoveAll(tmpFolder)
   307  		createTaskReportFile(t, tmpFolder)
   308  
   309  		sonar = sonarSettings{
   310  			workingDir:  tmpFolder,
   311  			binary:      "sonar-scanner",
   312  			environment: []string{},
   313  			options:     []string{},
   314  		}
   315  		options := sonarExecuteScanOptions{
   316  			ProjectKey:          "mock-project-key",
   317  			M2Path:              "my/custom/m2", // assumed to be resolved via alias from mavenExecute
   318  			InferJavaLibraries:  true,
   319  			CoverageExclusions:  []string{"one", "**/two", "three**"},
   320  			PullRequestProvider: "GitHub",
   321  		}
   322  		GeneralConfig.Verbose = true
   323  		defer func() { GeneralConfig.Verbose = false }()
   324  		fileUtilsExists = mockFileUtilsExists(true)
   325  		defer func() {
   326  			fileUtilsExists = FileUtils.FileExists
   327  		}()
   328  		// test
   329  		err = runSonar(options, &mockDownloadClient, &mockRunner, apiClient, &sonarExecuteScanInflux{})
   330  		// assert
   331  		assert.NoError(t, err)
   332  		assert.Contains(t, sonar.options, "-Dsonar.projectKey=mock-project-key")
   333  		assert.Contains(t, sonar.options, fmt.Sprintf("-Dsonar.java.libraries=%s",
   334  			filepath.Join("my/custom/m2", "**")))
   335  		assert.Contains(t, sonar.options, "-Dsonar.coverage.exclusions=one,**/two,three**")
   336  		assert.Contains(t, sonar.options, "-Dsonar.verbose=true")
   337  	})
   338  }
   339  
   340  func TestSonarHandlePullRequest(t *testing.T) {
   341  	t.Run("default", func(t *testing.T) {
   342  		// init
   343  		sonar = sonarSettings{
   344  			binary:      "sonar-scanner",
   345  			environment: []string{},
   346  			options:     []string{},
   347  		}
   348  		options := sonarExecuteScanOptions{
   349  			ChangeID:            "123",
   350  			PullRequestProvider: "GitHub",
   351  			ChangeBranch:        "feat/bogus",
   352  			ChangeTarget:        "master",
   353  			Owner:               "SAP",
   354  			Repository:          "jenkins-library",
   355  		}
   356  		// test
   357  		err := handlePullRequest(options)
   358  		// assert
   359  		assert.NoError(t, err)
   360  		assert.Contains(t, sonar.options, "sonar.pullrequest.key=123")
   361  		assert.Contains(t, sonar.options, "sonar.pullrequest.provider=github")
   362  		assert.Contains(t, sonar.options, "sonar.pullrequest.base=master")
   363  		assert.Contains(t, sonar.options, "sonar.pullrequest.branch=feat/bogus")
   364  		assert.Contains(t, sonar.options, "sonar.pullrequest.github.repository=SAP/jenkins-library")
   365  	})
   366  	t.Run("unsupported scm provider", func(t *testing.T) {
   367  		// init
   368  		sonar = sonarSettings{
   369  			binary:      "sonar-scanner",
   370  			environment: []string{},
   371  			options:     []string{},
   372  		}
   373  		options := sonarExecuteScanOptions{
   374  			ChangeID:            "123",
   375  			PullRequestProvider: "Gerrit",
   376  		}
   377  		// test
   378  		err := handlePullRequest(options)
   379  		// assert
   380  		assert.Error(t, err)
   381  		assert.Equal(t, "Pull-Request provider 'gerrit' is not supported!", err.Error())
   382  	})
   383  	t.Run("legacy", func(t *testing.T) {
   384  		// init
   385  		sonar = sonarSettings{
   386  			binary:      "sonar-scanner",
   387  			environment: []string{},
   388  			options:     []string{},
   389  		}
   390  		options := sonarExecuteScanOptions{
   391  			LegacyPRHandling:      true,
   392  			ChangeID:              "123",
   393  			Owner:                 "SAP",
   394  			Repository:            "jenkins-library",
   395  			GithubToken:           "some-token",
   396  			DisableInlineComments: true,
   397  		}
   398  		// test
   399  		err := handlePullRequest(options)
   400  		// assert
   401  		assert.NoError(t, err)
   402  		assert.Contains(t, sonar.options, "sonar.analysis.mode=preview")
   403  		assert.Contains(t, sonar.options, "sonar.github.pullRequest=123")
   404  		assert.Contains(t, sonar.options, "sonar.github.oauth=some-token")
   405  		assert.Contains(t, sonar.options, "sonar.github.repository=SAP/jenkins-library")
   406  		assert.Contains(t, sonar.options, "sonar.github.disableInlineComments=true")
   407  	})
   408  }
   409  
   410  func TestSonarLoadScanner(t *testing.T) {
   411  	mockClient := mockDownloader{shouldFail: false}
   412  
   413  	t.Run("use preinstalled sonar-scanner", func(t *testing.T) {
   414  		// init
   415  		ignore := ""
   416  		sonar = sonarSettings{
   417  			binary:      "local-sonar-scanner",
   418  			environment: []string{},
   419  			options:     []string{},
   420  		}
   421  		execLookPath = mockExecLookPath
   422  		defer func() { execLookPath = exec.LookPath }()
   423  		// test
   424  		err := loadSonarScanner(ignore, &mockClient)
   425  		// assert
   426  		assert.NoError(t, err)
   427  		assert.Equal(t, "local-sonar-scanner", sonar.binary)
   428  	})
   429  
   430  	t.Run("use downloaded sonar-scanner", func(t *testing.T) {
   431  		// init
   432  		url := "https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.6.2.2472-linux.zip"
   433  		sonar = sonarSettings{
   434  			binary:      "sonar-scanner",
   435  			environment: []string{},
   436  			options:     []string{},
   437  		}
   438  		execLookPath = mockExecLookPath
   439  		fileUtilsUnzip = mockFileUtilsUnzip(t, "sonar-scanner-cli-4.6.2.2472-linux.zip")
   440  		osRename = mockOsRename(t, "sonar-scanner-4.6.2.2472-linux", ".sonar-scanner")
   441  		defer func() {
   442  			execLookPath = exec.LookPath
   443  			fileUtilsUnzip = FileUtils.Unzip
   444  			osRename = os.Rename
   445  		}()
   446  		// test
   447  		err := loadSonarScanner(url, &mockClient)
   448  		// assert
   449  		assert.NoError(t, err)
   450  		assert.Equal(t, url, mockClient.requestedURL[0])
   451  		assert.Regexp(t, "sonar-scanner-cli-4.6.2.2472-linux.zip$", mockClient.requestedFile[0])
   452  		assert.Equal(t, filepath.Join(getWorkingDir(), ".sonar-scanner", "bin", "sonar-scanner"), sonar.binary)
   453  	})
   454  }
   455  
   456  func TestSonarLoadCertificates(t *testing.T) {
   457  	mockRunner := mock.ExecMockRunner{}
   458  	mockClient := mockDownloader{shouldFail: false}
   459  
   460  	t.Run("use local trust store", func(t *testing.T) {
   461  		// init
   462  		sonar = sonarSettings{
   463  			binary:      "sonar-scanner",
   464  			environment: []string{},
   465  			options:     []string{},
   466  		}
   467  		fileUtilsExists = mockFileUtilsExists(true)
   468  		defer func() { fileUtilsExists = FileUtils.FileExists }()
   469  		// test
   470  		err := loadCertificates([]string{}, &mockClient, &mockRunner)
   471  		// assert
   472  		assert.NoError(t, err)
   473  		assert.Contains(t, sonar.environment, "SONAR_SCANNER_OPTS=-Djavax.net.ssl.trustStore="+filepath.Join(getWorkingDir(), ".certificates", "cacerts")+" -Djavax.net.ssl.trustStorePassword=changeit")
   474  	})
   475  
   476  	t.Run("use local trust store with downloaded certificates", func(t *testing.T) {
   477  		// init
   478  		sonar = sonarSettings{
   479  			binary:      "sonar-scanner",
   480  			environment: []string{},
   481  			options:     []string{},
   482  		}
   483  		fileUtilsExists = mockFileUtilsExists(false)
   484  		// test
   485  		err := loadCertificates([]string{"https://sap.com/custom-1.crt", "https://sap.com/custom-2.crt"}, &mockClient, &mockRunner)
   486  		// assert
   487  		assert.NoError(t, err)
   488  		assert.Equal(t, "https://sap.com/custom-1.crt", mockClient.requestedURL[0])
   489  		assert.Equal(t, "https://sap.com/custom-2.crt", mockClient.requestedURL[1])
   490  		assert.Regexp(t, "custom-1.crt$", mockClient.requestedFile[0])
   491  		assert.Regexp(t, "custom-2.crt$", mockClient.requestedFile[1])
   492  		assert.Contains(t, sonar.environment, "SONAR_SCANNER_OPTS=-Djavax.net.ssl.trustStore="+filepath.Join(getWorkingDir(), ".certificates", "cacerts")+" -Djavax.net.ssl.trustStorePassword=changeit")
   493  	})
   494  
   495  	t.Run("use no trust store", func(t *testing.T) {
   496  		// init
   497  		sonar = sonarSettings{
   498  			binary:      "sonar-scanner",
   499  			environment: []string{},
   500  			options:     []string{},
   501  		}
   502  		fileUtilsExists = mockFileUtilsExists(false)
   503  		// test
   504  		err := loadCertificates([]string{}, &mockClient, &mockRunner)
   505  		// assert
   506  		assert.NoError(t, err)
   507  		assert.Empty(t, sonar.environment)
   508  	})
   509  }