github.com/SAP/jenkins-library@v1.362.0/cmd/sonarExecuteScan_test.go (about)

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