/* * SonarQube * Copyright (C) 2009-2025 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.scanner.report; import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Optional; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.slf4j.event.Level; import org.sonar.api.batch.bootstrap.ProjectDefinition; import org.sonar.api.batch.fs.internal.DefaultInputModule; import org.sonar.api.impl.utils.JUnitTempFolder; import org.sonar.api.notifications.AnalysisWarnings; import org.sonar.api.platform.Server; import org.sonar.api.testfixtures.log.LogTester; import org.sonar.api.utils.MessageException; import org.sonar.api.utils.TempFolder; import org.sonar.scanner.bootstrap.GlobalAnalysisMode; import org.sonar.scanner.ci.CiConfiguration; import org.sonar.scanner.ci.DevOpsPlatformInfo; import org.sonar.scanner.fs.InputModuleHierarchy; import org.sonar.scanner.http.DefaultScannerWsClient; import org.sonar.scanner.protocol.output.FileStructure; import org.sonar.scanner.protocol.output.ScannerReport; import org.sonar.scanner.scan.ScanProperties; import org.sonar.scanner.scan.branch.BranchConfiguration; import org.sonarqube.ws.Ce; import org.sonarqube.ws.client.HttpException; import org.sonarqube.ws.client.MockWsResponse; import org.sonarqube.ws.client.WsRequest; import org.sonarqube.ws.client.WsResponse; import static org.apache.commons.io.FileUtils.readFileToString; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static org.sonar.scanner.report.ReportPublisher.SUPPORT_OF_32_BIT_JRE_IS_DEPRECATED_MESSAGE; import static org.sonar.scanner.scan.branch.BranchType.BRANCH; import static org.sonar.scanner.scan.branch.BranchType.PULL_REQUEST; public class ReportPublisherTest { @Rule public LogTester logTester = new LogTester(); @Rule public JUnitTempFolder reportTempFolder = new JUnitTempFolder(); private GlobalAnalysisMode mode = mock(GlobalAnalysisMode.class); private ScanProperties properties = mock(ScanProperties.class); private DefaultScannerWsClient wsClient = mock(DefaultScannerWsClient.class, Mockito.RETURNS_DEEP_STUBS); private Server server = mock(Server.class); private InputModuleHierarchy moduleHierarchy = mock(InputModuleHierarchy.class); private DefaultInputModule root; private AnalysisContextReportPublisher contextPublisher = mock(AnalysisContextReportPublisher.class); private BranchConfiguration branchConfiguration = mock(BranchConfiguration.class); private CeTaskReportDataHolder reportMetadataHolder = mock(CeTaskReportDataHolder.class); private CiConfiguration ciConfiguration = mock(CiConfiguration.class); private ReportPublisher underTest; private AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class); private FileStructure fileStructure; private JavaArchitectureInformationProvider javaArchitectureInformationProvider = mock(JavaArchitectureInformationProvider.class); @Before public void setUp() { logTester.setLevel(Level.DEBUG); root = new DefaultInputModule( ProjectDefinition.create().setKey("org.sonarsource.sonarqube:sonarqube").setBaseDir(reportTempFolder.newDir()).setWorkDir(reportTempFolder.getRoot())); when(moduleHierarchy.root()).thenReturn(root); fileStructure = new FileStructure(reportTempFolder.getRoot()); when(server.getPublicRootUrl()).thenReturn("https://localhost"); when(server.getVersion()).thenReturn("6.4"); when(properties.metadataFilePath()).thenReturn(reportTempFolder.newDir().toPath() .resolve("folder") .resolve("report-task.txt")); underTest = new ReportPublisher(properties, wsClient, server, contextPublisher, moduleHierarchy, mode, reportTempFolder, new ReportPublisherStep[0], branchConfiguration, reportMetadataHolder, analysisWarnings, javaArchitectureInformationProvider, fileStructure, ciConfiguration); } @Test public void checks_if_component_has_issues() { underTest.getWriter().writeComponentIssues(1, List.of(ScannerReport.Issue.newBuilder().build())); assertThat(underTest.getReader().hasIssues(1)).isTrue(); assertThat(underTest.getReader().hasIssues(2)).isFalse(); } @Test public void use_write_timeout_from_properties() { when(properties.reportPublishTimeout()).thenReturn(60); MockWsResponse submitMockResponse = new MockWsResponse(); submitMockResponse.setContent(Ce.SubmitResponse.newBuilder().setTaskId("task-1234").build().toByteArray()); when(wsClient.call(any())).thenReturn(submitMockResponse); underTest.start(); underTest.execute(); verify(wsClient).call(argThat(req -> (req).getWriteTimeOutInMs().orElse(0) == 60_000)); } @Test public void should_not_log_success_when_should_wait_for_QG() { when(properties.shouldWaitForQualityGate()).thenReturn(true); MockWsResponse submitMockResponse = new MockWsResponse(); submitMockResponse.setContent(Ce.SubmitResponse.newBuilder().setTaskId("task-1234").build().toByteArray()); when(wsClient.call(any())).thenReturn(submitMockResponse); underTest.start(); underTest.execute(); assertThat(logTester.logs()).noneMatch(s -> s.contains("ANALYSIS SUCCESSFUL")); } @Test public void dump_information_about_report_uploading() throws IOException { underTest.prepareAndDumpMetadata("TASK-123"); assertThat(readFileToString(properties.metadataFilePath().toFile(), StandardCharsets.UTF_8)).isEqualTo(""" projectKey=org.sonarsource.sonarqube:sonarqube serverUrl=https://localhost serverVersion=6.4 dashboardUrl=https://localhost/dashboard?id=org.sonarsource.sonarqube%3Asonarqube ceTaskId=TASK-123 ceTaskUrl=https://localhost/api/ce/task?id=TASK-123 """); } @Test public void upload_error_message() { HttpException ex = new HttpException("url", 404, "{\"errors\":[{\"msg\":\"Organization with key 'MyOrg' does not exist\"}]}"); WsResponse response = mock(WsResponse.class); when(response.failIfNotSuccessful()).thenThrow(ex); when(wsClient.call(any(WsRequest.class))).thenThrow(new IllegalStateException("timeout")); assertThatThrownBy(() -> underTest.upload(reportTempFolder.newFile())) .isInstanceOf(IllegalStateException.class) .hasMessage("Failed to upload report: timeout"); } @Test public void parse_upload_error_message() { HttpException ex = new HttpException("url", 404, "{\"errors\":[{\"msg\":\"Organization with key 'MyOrg' does not exist\"}]}"); WsResponse response = mock(WsResponse.class); when(response.failIfNotSuccessful()).thenThrow(ex); when(wsClient.call(any(WsRequest.class))).thenReturn(response); assertThatThrownBy(() -> underTest.upload(reportTempFolder.newFile())) .isInstanceOf(MessageException.class) .hasMessage("Server failed to process report. Please check server logs: Organization with key 'MyOrg' does not exist"); } @Test public void dump_public_url_if_defined_for_main_branch() throws IOException { when(server.getPublicRootUrl()).thenReturn("https://publicserver/sonarqube"); underTest.prepareAndDumpMetadata("TASK-123"); assertThat(readFileToString(properties.metadataFilePath().toFile(), StandardCharsets.UTF_8)).isEqualTo(""" projectKey=org.sonarsource.sonarqube:sonarqube serverUrl=https://publicserver/sonarqube serverVersion=6.4 dashboardUrl=https://publicserver/sonarqube/dashboard?id=org.sonarsource.sonarqube%3Asonarqube ceTaskId=TASK-123 ceTaskUrl=https://publicserver/sonarqube/api/ce/task?id=TASK-123 """); } @Test public void dump_public_url_if_defined_for_branches() throws IOException { when(server.getPublicRootUrl()).thenReturn("https://publicserver/sonarqube"); when(branchConfiguration.branchType()).thenReturn(BRANCH); when(branchConfiguration.branchName()).thenReturn("branch-6.7"); ReportPublisher underTest = new ReportPublisher(properties, wsClient, server, contextPublisher, moduleHierarchy, mode, mock(TempFolder.class), new ReportPublisherStep[0], branchConfiguration, reportMetadataHolder, analysisWarnings, javaArchitectureInformationProvider, fileStructure, ciConfiguration); underTest.prepareAndDumpMetadata("TASK-123"); assertThat(readFileToString(properties.metadataFilePath().toFile(), StandardCharsets.UTF_8)).isEqualTo(""" projectKey=org.sonarsource.sonarqube:sonarqube serverUrl=https://publicserver/sonarqube serverVersion=6.4 dashboardUrl=https://publicserver/sonarqube/dashboard?id=org.sonarsource.sonarqube%3Asonarqube&branch=branch-6.7 ceTaskId=TASK-123 ceTaskUrl=https://publicserver/sonarqube/api/ce/task?id=TASK-123 """); } @Test public void dump_public_url_if_defined_for_pull_request() throws IOException { when(server.getPublicRootUrl()).thenReturn("https://publicserver/sonarqube"); when(branchConfiguration.branchName()).thenReturn("Bitbucket cloud Widget"); when(branchConfiguration.branchType()).thenReturn(PULL_REQUEST); when(branchConfiguration.pullRequestKey()).thenReturn("105"); ReportPublisher underTest = new ReportPublisher(properties, wsClient, server, contextPublisher, moduleHierarchy, mode, mock(TempFolder.class), new ReportPublisherStep[0], branchConfiguration, reportMetadataHolder, analysisWarnings, javaArchitectureInformationProvider, fileStructure, ciConfiguration); underTest.prepareAndDumpMetadata("TASK-123"); assertThat(readFileToString(properties.metadataFilePath().toFile(), StandardCharsets.UTF_8)).isEqualTo(""" projectKey=org.sonarsource.sonarqube:sonarqube serverUrl=https://publicserver/sonarqube serverVersion=6.4 dashboardUrl=https://publicserver/sonarqube/dashboard?id=org.sonarsource.sonarqube%3Asonarqube&pullRequest=105 ceTaskId=TASK-123 ceTaskUrl=https://publicserver/sonarqube/api/ce/task?id=TASK-123 """); } @Test public void fail_if_public_url_malformed() { when(server.getPublicRootUrl()).thenReturn("invalid"); assertThatThrownBy(() -> underTest.start()) .isInstanceOf(MessageException.class) .hasMessage("Failed to parse public URL set in SonarQube server: invalid"); } @Test public void should_not_dump_information_when_medium_test_enabled() { when(mode.isMediumTest()).thenReturn(true); underTest.start(); underTest.execute(); assertThat(logTester.logs(Level.INFO)) .contains("ANALYSIS SUCCESSFUL") .doesNotContain("dashboard/index"); assertThat(properties.metadataFilePath()).doesNotExist(); } @Test public void should_upload_and_dump_information() { when(reportMetadataHolder.getDashboardUrl()).thenReturn("https://publicserver/sonarqube/dashboard?id=org.sonarsource.sonarqube%3Asonarqube"); when(reportMetadataHolder.getCeTaskUrl()).thenReturn("https://publicserver/sonarqube/api/ce/task?id=TASK-123"); MockWsResponse submitMockResponse = new MockWsResponse(); submitMockResponse.setContent(Ce.SubmitResponse.newBuilder().setTaskId("task-1234").build().toByteArray()); when(wsClient.call(any())).thenReturn(submitMockResponse); underTest.start(); underTest.execute(); assertThat(properties.metadataFilePath()).exists(); assertThat(logTester.logs(Level.DEBUG)) .contains("Report metadata written to " + properties.metadataFilePath()); assertThat(logTester.logs(Level.INFO)) .contains("ANALYSIS SUCCESSFUL, you can find the results at: https://publicserver/sonarqube/dashboard?id=org.sonarsource.sonarqube%3Asonarqube") .contains("More about the report processing at https://publicserver/sonarqube/api/ce/task?id=TASK-123"); } @Test public void dump_information_to_custom_path() { underTest.prepareAndDumpMetadata("TASK-123"); assertThat(properties.metadataFilePath()).exists(); assertThat(logTester.logs(Level.DEBUG)).contains("Report metadata written to " + properties.metadataFilePath()); } @Test public void should_not_delete_report_if_property_is_set() throws IOException { when(properties.shouldKeepReport()).thenReturn(true); underTest.start(); underTest.stop(); assertThat(fileStructure.root()).isDirectory(); } @Test public void should_delete_report_by_default() throws IOException { underTest.start(); underTest.stop(); assertThat(fileStructure.root()).doesNotExist(); } @Test public void test_ws_parameters() throws Exception { WsResponse response = mock(WsResponse.class); PipedOutputStream out = new PipedOutputStream(); PipedInputStream in = new PipedInputStream(out); Ce.SubmitResponse.newBuilder().build().writeTo(out); out.close(); when(response.failIfNotSuccessful()).thenReturn(response); when(response.contentStream()).thenReturn(in); when(wsClient.call(any(WsRequest.class))).thenReturn(response); underTest.upload(reportTempFolder.newFile()); ArgumentCaptor capture = ArgumentCaptor.forClass(WsRequest.class); verify(wsClient).call(capture.capture()); WsRequest wsRequest = capture.getValue(); assertThat(wsRequest.getParameters().getKeys()).containsOnly("projectKey"); assertThat(wsRequest.getParameters().getValue("projectKey")).isEqualTo("org.sonarsource.sonarqube:sonarqube"); assertThat(wsRequest.getParameters().getValues("characteristic")).isEmpty(); } @Test public void test_send_branches_characteristics() throws Exception { String branchName = "feature"; when(branchConfiguration.branchName()).thenReturn(branchName); when(branchConfiguration.branchType()).thenReturn(BRANCH); WsResponse response = mock(WsResponse.class); PipedOutputStream out = new PipedOutputStream(); PipedInputStream in = new PipedInputStream(out); Ce.SubmitResponse.newBuilder().build().writeTo(out); out.close(); when(response.failIfNotSuccessful()).thenReturn(response); when(response.contentStream()).thenReturn(in); when(wsClient.call(any(WsRequest.class))).thenReturn(response); underTest.upload(reportTempFolder.newFile()); ArgumentCaptor capture = ArgumentCaptor.forClass(WsRequest.class); verify(wsClient).call(capture.capture()); WsRequest wsRequest = capture.getValue(); assertThat(wsRequest.getParameters().getKeys()).hasSize(2); assertThat(wsRequest.getParameters().getValues("projectKey")).containsExactly("org.sonarsource.sonarqube:sonarqube"); assertThat(wsRequest.getParameters().getValues("characteristic")) .containsExactlyInAnyOrder("branch=" + branchName, "branchType=" + BRANCH.name()); } @Test public void send_pull_request_characteristic() throws Exception { String branchName = "feature"; String pullRequestId = "pr-123"; when(branchConfiguration.branchName()).thenReturn(branchName); when(branchConfiguration.branchType()).thenReturn(PULL_REQUEST); when(branchConfiguration.pullRequestKey()).thenReturn(pullRequestId); WsResponse response = mock(WsResponse.class); PipedOutputStream out = new PipedOutputStream(); PipedInputStream in = new PipedInputStream(out); Ce.SubmitResponse.newBuilder().build().writeTo(out); out.close(); when(response.failIfNotSuccessful()).thenReturn(response); when(response.contentStream()).thenReturn(in); when(wsClient.call(any(WsRequest.class))).thenReturn(response); underTest.upload(reportTempFolder.newFile()); ArgumentCaptor capture = ArgumentCaptor.forClass(WsRequest.class); verify(wsClient).call(capture.capture()); WsRequest wsRequest = capture.getValue(); assertThat(wsRequest.getParameters().getKeys()).hasSize(2); assertThat(wsRequest.getParameters().getValues("projectKey")).containsExactly("org.sonarsource.sonarqube:sonarqube"); assertThat(wsRequest.getParameters().getValues("characteristic")) .containsExactlyInAnyOrder("pullRequest=" + pullRequestId); } @Test public void upload_whenDevOpsPlatformInformationPresentInCiConfiguration_shouldUploadDevOpsPlatformInfoAsCharacteristic() throws Exception { String branchName = "feature"; String pullRequestId = "pr-123"; DevOpsPlatformInfo devOpsPlatformInfo = new DevOpsPlatformInfo("https://devops.example.com", "projectId"); when(branchConfiguration.branchName()).thenReturn(branchName); when(branchConfiguration.branchType()).thenReturn(PULL_REQUEST); when(branchConfiguration.pullRequestKey()).thenReturn(pullRequestId); when(ciConfiguration.getDevOpsPlatformInfo()).thenReturn(Optional.of(devOpsPlatformInfo)); WsResponse response = mock(WsResponse.class); PipedOutputStream out = new PipedOutputStream(); PipedInputStream in = new PipedInputStream(out); Ce.SubmitResponse.newBuilder().build().writeTo(out); out.close(); when(response.failIfNotSuccessful()).thenReturn(response); when(response.contentStream()).thenReturn(in); when(wsClient.call(any(WsRequest.class))).thenReturn(response); underTest.upload(reportTempFolder.newFile()); ArgumentCaptor capture = ArgumentCaptor.forClass(WsRequest.class); verify(wsClient).call(capture.capture()); WsRequest wsRequest = capture.getValue(); assertThat(wsRequest.getParameters().getValues("characteristic")) .contains( "devOpsPlatformUrl=" + devOpsPlatformInfo.getUrl(), "devOpsPlatformProjectIdentifier=" + devOpsPlatformInfo.getProjectIdentifier()); } @Test public void test_do_not_log_or_add_warning_if_using_64bit_jre() { when(javaArchitectureInformationProvider.is64bitJavaVersion()).thenReturn(true); when(mode.isMediumTest()).thenReturn(true); underTest.start(); underTest.execute(); assertThat(logTester.logs(Level.WARN)).isEmpty(); verifyNoInteractions(analysisWarnings); } @Test public void test_log_and_add_warning_if_using_non64bit_jre() { when(javaArchitectureInformationProvider.is64bitJavaVersion()).thenReturn(false); when(mode.isMediumTest()).thenReturn(true); underTest.start(); underTest.execute(); assertThat(logTester.logs(Level.WARN)).containsOnly(SUPPORT_OF_32_BIT_JRE_IS_DEPRECATED_MESSAGE); verify(analysisWarnings).addUnique(SUPPORT_OF_32_BIT_JRE_IS_DEPRECATED_MESSAGE); } }