package cmd import ( "fmt" "io/ioutil" "net/http" "os" "os/exec" "path/filepath" "testing" "github.com/bmatcuk/doublestar" "github.com/jarcoal/httpmock" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" piperHttp "github.com/SAP/jenkins-library/pkg/http" "github.com/SAP/jenkins-library/pkg/mock" FileUtils "github.com/SAP/jenkins-library/pkg/piperutils" SonarUtils "github.com/SAP/jenkins-library/pkg/sonar" ) //TODO: extract to mock package type mockDownloader struct { shouldFail bool requestedURL []string requestedFile []string } func (m *mockDownloader) DownloadFile(url, filename string, header http.Header, cookies []*http.Cookie) error { m.requestedURL = append(m.requestedURL, url) m.requestedFile = append(m.requestedFile, filename) if m.shouldFail { return errors.New("something happened") } return nil } func (m *mockDownloader) SetOptions(options piperHttp.ClientOptions) {} func mockFileUtilsExists(exists bool) func(string) (bool, error) { return func(filename string) (bool, error) { if exists { return true, nil } return false, errors.New("something happened") } } func mockExecLookPath(executable string) (string, error) { if executable == "local-sonar-scanner" { return "/usr/bin/sonar-scanner", nil } return "", errors.New("something happened") } func mockFileUtilsUnzip(t *testing.T, expectSrc string) func(string, string) ([]string, error) { return func(src, dest string) ([]string, error) { assert.Equal(t, filepath.Join(dest, expectSrc), src) return []string{}, nil } } func mockOsRename(t *testing.T, expectOld, expectNew string) func(string, string) error { return func(old, new string) error { assert.Regexp(t, expectOld, old) assert.Equal(t, expectNew, new) return nil } } func mockOsStat(exists map[string]bool) func(name string) (os.FileInfo, error) { return func(name string) (os.FileInfo, error) { _, exists := exists[name] if exists { // Exploits the fact that FileInfo result from os.Stat() is ignored anyway return nil, nil } return nil, errors.New("something happened") } } func mockGlob(matchesForPatterns map[string][]string) func(pattern string) ([]string, error) { return func(pattern string) ([]string, error) { matches, exists := matchesForPatterns[pattern] if exists { return matches, nil } return nil, errors.New("something happened") } } func createTaskReportFile(t *testing.T, workingDir string) { require.NoError(t, os.MkdirAll(filepath.Join(workingDir, ".scannerwork"), 0755)) require.NoError(t, ioutil.WriteFile(filepath.Join(workingDir, ".scannerwork", "report-task.txt"), []byte(taskReportContent), 0755)) require.FileExists(t, filepath.Join(workingDir, ".scannerwork", "report-task.txt")) } const sonarServerURL = "https://sonarcloud.io" const taskReportContent = ` projectKey=piper-test serverUrl=` + sonarServerURL + ` serverVersion=8.0.0.12345 dashboardUrl=` + sonarServerURL + `/dashboard/index/piper-test ceTaskId=AXERR2JBbm9IiM5TEST ceTaskUrl=` + sonarServerURL + `/api/ce/task?id=AXERR2JBbm9IiMTEST ` func TestRunSonar(t *testing.T) { mockRunner := mock.ExecMockRunner{} mockDownloadClient := mockDownloader{shouldFail: false} apiClient := &piperHttp.Client{} apiClient.SetOptions(piperHttp.ClientOptions{MaxRetries: -1, UseDefaultTransport: true}) // mock SonarQube API calls httpmock.Activate() defer httpmock.DeactivateAndReset() // add response handler httpmock.RegisterResponder(http.MethodGet, sonarServerURL+"/api/"+SonarUtils.EndpointCeTask+"", httpmock.NewStringResponder(http.StatusOK, `{ "task": { "componentId": "AXERR2JBbm9IiM5TEST", "status": "SUCCESS" }}`)) httpmock.RegisterResponder(http.MethodGet, sonarServerURL+"/api/"+SonarUtils.EndpointIssuesSearch+"", httpmock.NewStringResponder(http.StatusOK, `{ "total": 0 }`)) t.Run("default", func(t *testing.T) { // init tmpFolder, err := ioutil.TempDir(".", "test-sonar-") require.NoError(t, err) defer os.RemoveAll(tmpFolder) createTaskReportFile(t, tmpFolder) sonar = sonarSettings{ workingDir: tmpFolder, binary: "sonar-scanner", environment: []string{}, options: []string{}, } options := sonarExecuteScanOptions{ CustomTLSCertificateLinks: []string{}, Token: "secret-ABC", ServerURL: sonarServerURL, Organization: "SAP", Version: "1.2.3", VersioningModel: "major", } fileUtilsExists = mockFileUtilsExists(true) // test err = runSonar(options, &mockDownloadClient, &mockRunner, apiClient, &sonarExecuteScanInflux{}) // assert assert.NoError(t, err) assert.Contains(t, sonar.options, "-Dsonar.projectVersion=1") assert.Contains(t, sonar.options, "-Dsonar.organization=SAP") assert.Contains(t, sonar.environment, "SONAR_HOST_URL="+sonarServerURL) assert.Contains(t, sonar.environment, "SONAR_TOKEN=secret-ABC") assert.Contains(t, sonar.environment, "SONAR_SCANNER_OPTS=-Djavax.net.ssl.trustStore="+filepath.Join(getWorkingDir(), ".certificates", "cacerts")+" -Djavax.net.ssl.trustStorePassword=changeit") assert.FileExists(t, filepath.Join(sonar.workingDir, "sonarExecuteScan_reports.json")) assert.FileExists(t, filepath.Join(sonar.workingDir, "sonarExecuteScan_links.json")) }) t.Run("with custom options", func(t *testing.T) { // init tmpFolder, err := ioutil.TempDir(".", "test-sonar-") require.NoError(t, err) defer os.RemoveAll(tmpFolder) createTaskReportFile(t, tmpFolder) sonar = sonarSettings{ workingDir: tmpFolder, binary: "sonar-scanner", environment: []string{}, options: []string{}, } options := sonarExecuteScanOptions{ Options: []string{"-Dsonar.projectKey=piper"}, } fileUtilsExists = mockFileUtilsExists(true) defer func() { fileUtilsExists = FileUtils.FileExists }() // test err = runSonar(options, &mockDownloadClient, &mockRunner, apiClient, &sonarExecuteScanInflux{}) // assert assert.NoError(t, err) assert.Contains(t, sonar.options, "-Dsonar.projectKey=piper") }) t.Run("with binaries option", func(t *testing.T) { // init tmpFolder, err := ioutil.TempDir(".", "test-sonar-") require.NoError(t, err) defer func() { _ = os.RemoveAll(tmpFolder) }() createTaskReportFile(t, tmpFolder) sonar = sonarSettings{ workingDir: tmpFolder, binary: "sonar-scanner", environment: []string{}, options: []string{}, } fileUtilsExists = mockFileUtilsExists(true) globMatches := make(map[string][]string) globMatches[pomXMLPattern] = []string{"pom.xml", "application/pom.xml"} doublestarGlob = mockGlob(globMatches) existsMap := make(map[string]bool) existsMap[filepath.Join("target", "classes")] = true existsMap[filepath.Join("target", "test-classes")] = true existsMap[filepath.Join("application", "target", "classes")] = true osStat = mockOsStat(existsMap) defer func() { fileUtilsExists = FileUtils.FileExists doublestarGlob = doublestar.Glob osStat = os.Stat }() options := sonarExecuteScanOptions{ InferJavaBinaries: true, } // test err = runSonar(options, &mockDownloadClient, &mockRunner, apiClient, &sonarExecuteScanInflux{}) // assert assert.NoError(t, err) assert.Contains(t, sonar.options, fmt.Sprintf("-Dsonar.java.binaries=%s,%s,%s", filepath.Join("target", "classes"), filepath.Join("target", "test-classes"), filepath.Join("application", "target", "classes"))) }) t.Run("with binaries option already given", func(t *testing.T) { // init tmpFolder, err := ioutil.TempDir(".", "test-sonar-") require.NoError(t, err) defer func() { _ = os.RemoveAll(tmpFolder) }() createTaskReportFile(t, tmpFolder) sonar = sonarSettings{ workingDir: tmpFolder, binary: "sonar-scanner", environment: []string{}, options: []string{}, } fileUtilsExists = mockFileUtilsExists(true) globMatches := make(map[string][]string) globMatches[pomXMLPattern] = []string{"pom.xml"} doublestarGlob = mockGlob(globMatches) existsMap := make(map[string]bool) existsMap[filepath.Join("target", "classes")] = true osStat = mockOsStat(existsMap) defer func() { fileUtilsExists = FileUtils.FileExists doublestarGlob = doublestar.Glob osStat = os.Stat }() options := sonarExecuteScanOptions{ Options: []string{"-Dsonar.java.binaries=user/provided"}, InferJavaBinaries: true, } // test err = runSonar(options, &mockDownloadClient, &mockRunner, apiClient, &sonarExecuteScanInflux{}) // assert assert.NoError(t, err) assert.NotContains(t, sonar.options, fmt.Sprintf("-Dsonar.java.binaries=%s", filepath.Join("target", "classes"))) assert.Contains(t, sonar.options, "-Dsonar.java.binaries=user/provided") }) t.Run("projectKey, coverageExclusions, m2Path", func(t *testing.T) { // init tmpFolder, err := ioutil.TempDir(".", "test-sonar-") require.NoError(t, err) defer os.RemoveAll(tmpFolder) createTaskReportFile(t, tmpFolder) sonar = sonarSettings{ workingDir: tmpFolder, binary: "sonar-scanner", environment: []string{}, options: []string{}, } options := sonarExecuteScanOptions{ ProjectKey: "mock-project-key", M2Path: "my/custom/m2", // assumed to be resolved via alias from mavenExecute InferJavaLibraries: true, CoverageExclusions: []string{"one", "**/two", "three**"}, } fileUtilsExists = mockFileUtilsExists(true) defer func() { fileUtilsExists = FileUtils.FileExists }() // test err = runSonar(options, &mockDownloadClient, &mockRunner, apiClient, &sonarExecuteScanInflux{}) // assert assert.NoError(t, err) assert.Contains(t, sonar.options, "-Dsonar.projectKey=mock-project-key") assert.Contains(t, sonar.options, fmt.Sprintf("-Dsonar.java.libraries=%s", filepath.Join("my/custom/m2", "**"))) assert.Contains(t, sonar.options, "-Dsonar.coverage.exclusions=one,**/two,three**") }) } func TestSonarHandlePullRequest(t *testing.T) { t.Run("default", func(t *testing.T) { // init sonar = sonarSettings{ binary: "sonar-scanner", environment: []string{}, options: []string{}, } options := sonarExecuteScanOptions{ ChangeID: "123", PullRequestProvider: "GitHub", ChangeBranch: "feat/bogus", ChangeTarget: "master", Owner: "SAP", Repository: "jenkins-library", } // test err := handlePullRequest(options) // assert assert.NoError(t, err) assert.Contains(t, sonar.options, "sonar.pullrequest.key=123") assert.Contains(t, sonar.options, "sonar.pullrequest.provider=github") assert.Contains(t, sonar.options, "sonar.pullrequest.base=master") assert.Contains(t, sonar.options, "sonar.pullrequest.branch=feat/bogus") assert.Contains(t, sonar.options, "sonar.pullrequest.github.repository=SAP/jenkins-library") }) t.Run("unsupported scm provider", func(t *testing.T) { // init sonar = sonarSettings{ binary: "sonar-scanner", environment: []string{}, options: []string{}, } options := sonarExecuteScanOptions{ ChangeID: "123", PullRequestProvider: "Gerrit", } // test err := handlePullRequest(options) // assert assert.Error(t, err) assert.Equal(t, "Pull-Request provider 'gerrit' is not supported!", err.Error()) }) t.Run("legacy", func(t *testing.T) { // init sonar = sonarSettings{ binary: "sonar-scanner", environment: []string{}, options: []string{}, } options := sonarExecuteScanOptions{ LegacyPRHandling: true, ChangeID: "123", Owner: "SAP", Repository: "jenkins-library", GithubToken: "some-token", DisableInlineComments: true, } // test err := handlePullRequest(options) // assert assert.NoError(t, err) assert.Contains(t, sonar.options, "sonar.analysis.mode=preview") assert.Contains(t, sonar.options, "sonar.github.pullRequest=123") assert.Contains(t, sonar.options, "sonar.github.oauth=some-token") assert.Contains(t, sonar.options, "sonar.github.repository=SAP/jenkins-library") assert.Contains(t, sonar.options, "sonar.github.disableInlineComments=true") }) } func TestSonarLoadScanner(t *testing.T) { mockClient := mockDownloader{shouldFail: false} t.Run("use preinstalled sonar-scanner", func(t *testing.T) { // init ignore := "" sonar = sonarSettings{ binary: "local-sonar-scanner", environment: []string{}, options: []string{}, } execLookPath = mockExecLookPath defer func() { execLookPath = exec.LookPath }() // test err := loadSonarScanner(ignore, &mockClient) // assert assert.NoError(t, err) assert.Equal(t, "local-sonar-scanner", sonar.binary) }) t.Run("use downloaded sonar-scanner", func(t *testing.T) { // init url := "https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.4.0.2170-linux.zip" sonar = sonarSettings{ binary: "sonar-scanner", environment: []string{}, options: []string{}, } execLookPath = mockExecLookPath fileUtilsUnzip = mockFileUtilsUnzip(t, "sonar-scanner-cli-4.4.0.2170-linux.zip") osRename = mockOsRename(t, "sonar-scanner-4.4.0.2170-linux", ".sonar-scanner") defer func() { execLookPath = exec.LookPath fileUtilsUnzip = FileUtils.Unzip osRename = os.Rename }() // test err := loadSonarScanner(url, &mockClient) // assert assert.NoError(t, err) assert.Equal(t, url, mockClient.requestedURL[0]) assert.Regexp(t, "sonar-scanner-cli-4.4.0.2170-linux.zip$", mockClient.requestedFile[0]) assert.Equal(t, filepath.Join(getWorkingDir(), ".sonar-scanner", "bin", "sonar-scanner"), sonar.binary) }) } func TestSonarLoadCertificates(t *testing.T) { mockRunner := mock.ExecMockRunner{} mockClient := mockDownloader{shouldFail: false} t.Run("use local trust store", func(t *testing.T) { // init sonar = sonarSettings{ binary: "sonar-scanner", environment: []string{}, options: []string{}, } fileUtilsExists = mockFileUtilsExists(true) defer func() { fileUtilsExists = FileUtils.FileExists }() // test err := loadCertificates([]string{}, &mockClient, &mockRunner) // assert assert.NoError(t, err) assert.Contains(t, sonar.environment, "SONAR_SCANNER_OPTS=-Djavax.net.ssl.trustStore="+filepath.Join(getWorkingDir(), ".certificates", "cacerts")+" -Djavax.net.ssl.trustStorePassword=changeit") }) t.Run("use local trust store with downloaded certificates", func(t *testing.T) { // init sonar = sonarSettings{ binary: "sonar-scanner", environment: []string{}, options: []string{}, } fileUtilsExists = mockFileUtilsExists(false) // test err := loadCertificates([]string{"https://sap.com/custom-1.crt", "https://sap.com/custom-2.crt"}, &mockClient, &mockRunner) // assert assert.NoError(t, err) assert.Equal(t, "https://sap.com/custom-1.crt", mockClient.requestedURL[0]) assert.Equal(t, "https://sap.com/custom-2.crt", mockClient.requestedURL[1]) assert.Regexp(t, "custom-1.crt$", mockClient.requestedFile[0]) assert.Regexp(t, "custom-2.crt$", mockClient.requestedFile[1]) assert.Contains(t, sonar.environment, "SONAR_SCANNER_OPTS=-Djavax.net.ssl.trustStore="+filepath.Join(getWorkingDir(), ".certificates", "cacerts")+" -Djavax.net.ssl.trustStorePassword=changeit") }) t.Run("use no trust store", func(t *testing.T) { // init sonar = sonarSettings{ binary: "sonar-scanner", environment: []string{}, options: []string{}, } fileUtilsExists = mockFileUtilsExists(false) // test err := loadCertificates([]string{}, &mockClient, &mockRunner) // assert assert.NoError(t, err) assert.Empty(t, sonar.environment) }) }