1
0
mirror of https://github.com/mc1arke/sonarqube-community-branch-plugin.git synced 2024-11-30 09:06:46 +02:00

Adds support for Bitbucket Cloud Code Insights

This commit provides support for the newly created bitbucket cloud code insights
API endpoints. The implementation has been done under the consideration that in
newer versions no dedicated ALM support for bitbucket cloud exists, thus this
implementation is minimal invasive.

One thing to note here:

* For local testing the link on CloudCreateReportRequest has to be set manually to a non localhost URL since
bitbuckets API doesn't support it.
This commit is contained in:
Marvin Wichmann 2020-06-08 23:28:23 +01:00 committed by Michael Clarke
parent 54c7d4e4f4
commit 7a5ae26fbc
31 changed files with 1657 additions and 304 deletions

View File

@ -19,7 +19,7 @@ SonarQube Version | Plugin Version
# Features
The plugin is intended to support the [features and parameters specified in the SonarQube documentation](https://docs.sonarqube.org/latest/branches/overview/), with the following caveats
* __Pull Requests:__ Analysis of Pull Requests is fully supported, but the decoration of pull requests is only currently available for Github, Gitlab and Bitbucket Server
* __Pull Requests:__ Analysis of Pull Requests is fully supported, but the decoration of pull requests is only currently available for Github, Gitlab, Bitbucket Server and Bitbucket Cloud.
# Installation
Either build the project or [download a compatible release version of the plugin JAR](https://github.com/mc1arke/sonarqube-community-branch-plugin/releases). Copy the plugin JAR file to the `extensions/plugins/` **and** the `lib/common/` directories of your SonarQube instance and restart SonarQube.

View File

@ -20,8 +20,7 @@ package com.github.mc1arke.sonarqube.plugin.ce;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestPostAnalysisTask;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.BitbucketServerPullRequestDecorator;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.BitbucketClient;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.BitbucketPullRequestDecorator;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.GithubPullRequestDecorator;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3.DefaultLinkHeaderReader;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3.RestApplicationAuthenticationProvider;
@ -42,8 +41,7 @@ public class CommunityReportAnalysisComponentProvider implements ReportAnalysisC
return Arrays.asList(CommunityBranchLoaderDelegate.class, PullRequestPostAnalysisTask.class,
PostAnalysisIssueVisitor.class, GithubPullRequestDecorator.class,
GraphqlCheckRunProvider.class, DefaultLinkHeaderReader.class, RestApplicationAuthenticationProvider.class,
BitbucketServerPullRequestDecorator.class, BitbucketClient.class,
GitlabServerPullRequestDecorator.class);
BitbucketPullRequestDecorator.class, GitlabServerPullRequestDecorator.class);
}
}

View File

@ -22,12 +22,14 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.BitbucketClient;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.BitbucketClientFactory;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.BitbucketException;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.Annotation;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CreateAnnotationsRequest;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CreateReportRequest;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.BitbucketConfiguration;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CodeInsightsAnnotation;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.DataValue;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.ReportData;
import com.google.common.annotations.VisibleForTesting;
import org.sonar.api.ce.posttask.QualityGate;
import org.sonar.api.issue.Issue;
import org.sonar.api.measures.CoreMetrics;
@ -54,9 +56,9 @@ import java.util.stream.Collectors;
import static java.lang.String.format;
import static java.util.stream.Collectors.toSet;
public class BitbucketServerPullRequestDecorator implements PullRequestBuildStatusDecorator {
public class BitbucketPullRequestDecorator implements PullRequestBuildStatusDecorator {
private static final Logger LOGGER = Loggers.get(BitbucketServerPullRequestDecorator.class);
private static final Logger LOGGER = Loggers.get(BitbucketPullRequestDecorator.class);
private static final int DEFAULT_MAX_ANNOTATIONS = 1000;
@ -66,28 +68,34 @@ public class BitbucketServerPullRequestDecorator implements PullRequestBuildStat
Issue.STATUSES.stream().filter(s -> !Issue.STATUS_CLOSED.equals(s) && !Issue.STATUS_RESOLVED.equals(s))
.collect(Collectors.toList());
private final BitbucketClient client;
public BitbucketServerPullRequestDecorator(BitbucketClient client) {
this.client = client;
}
@Override
public DecorationResult decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) {
String project = projectAlmSettingDto.getAlmRepo();
String repo = projectAlmSettingDto.getAlmSlug();
String url = almSettingDto.getUrl();
String token = almSettingDto.getPersonalAccessToken();
BitbucketConfiguration bitbucketConfiguration = new BitbucketConfiguration(url, token, repo, project);
BitbucketClient client = createClient(bitbucketConfiguration);
try {
if(!client.supportsCodeInsights(almSettingDto)) {
LOGGER.warn("Your Bitbucket instances does not support the Code Insights API.");
if (!client.supportsCodeInsights()) {
LOGGER.warn("Your Bitbucket instance does not support the Code Insights API.");
return DEFAULT_DECORATION_RESULT;
}
String project = projectAlmSettingDto.getAlmRepo();
String repo = projectAlmSettingDto.getAlmSlug();
client.createReport(project, repo,
analysisDetails.getCommitSha(),
toReport(analysisDetails),
almSettingDto
CodeInsightsReport codeInsightsReport = client.createCodeInsightsReport(
toReport(client, analysisDetails),
reportDescription(analysisDetails),
analysisDetails.getAnalysisDate().toInstant(),
analysisDetails.getDashboardUrl(),
format("%s/common/icon.png", analysisDetails.getBaseImageUrl()),
analysisDetails.getQualityGateStatus()
);
updateAnnotations(project, repo, analysisDetails, almSettingDto);
client.uploadReport(project, repo,
analysisDetails.getCommitSha(), codeInsightsReport);
updateAnnotations(client, project, repo, analysisDetails);
} catch (IOException e) {
LOGGER.error("Could not decorate pull request for project {}", analysisDetails.getAnalysisProjectKey(), e);
}
@ -95,12 +103,17 @@ public class BitbucketServerPullRequestDecorator implements PullRequestBuildStat
return DEFAULT_DECORATION_RESULT;
}
@VisibleForTesting
BitbucketClient createClient(BitbucketConfiguration bitbucketConfiguration) {
return BitbucketClientFactory.createClient(bitbucketConfiguration);
}
@Override
public ALM alm() {
return ALM.BITBUCKET;
}
private CreateReportRequest toReport(AnalysisDetails analysisDetails) {
private List<ReportData> toReport(BitbucketClient client, AnalysisDetails analysisDetails) {
Map<RuleType, Long> rules = analysisDetails.countRuleByType();
List<ReportData> reportData = new ArrayList<>();
@ -109,31 +122,24 @@ public class BitbucketServerPullRequestDecorator implements PullRequestBuildStat
reportData.add(securityReport(rules.get(RuleType.VULNERABILITY), rules.get(RuleType.SECURITY_HOTSPOT)));
reportData.add(new ReportData("Duplication", new DataValue.Percentage(newDuplication(analysisDetails))));
reportData.add(maintainabilityReport(rules.get(RuleType.CODE_SMELL)));
reportData.add(new ReportData("Analysis details", new DataValue.Link("Go to SonarQube", analysisDetails.getDashboardUrl())));
reportData.add(new ReportData("Analysis details", client.createLinkDataValue(analysisDetails.getDashboardUrl())));
return new CreateReportRequest(reportData,
reportDescription(analysisDetails),
"SonarQube",
"SonarQube",
analysisDetails.getAnalysisDate().toInstant(),
analysisDetails.getDashboardUrl(),
format("%s/common/icon.png", analysisDetails.getBaseImageUrl()),
asInsightStatus(analysisDetails.getQualityGateStatus()));
return reportData;
}
private void updateAnnotations(String project, String repo, AnalysisDetails analysisDetails, AlmSettingDto almSettingDto) throws IOException {
private void updateAnnotations(BitbucketClient client, String project, String repo, AnalysisDetails analysisDetails) throws IOException {
final AtomicInteger chunkCounter = new AtomicInteger(0);
client.deleteAnnotations(project, repo, analysisDetails.getCommitSha(), almSettingDto);
client.deleteAnnotations(project, repo, analysisDetails.getCommitSha());
Map<Object, Set<Annotation>> annotationChunks = analysisDetails.getPostAnalysisIssueVisitor().getIssues().stream()
Map<Object, Set<CodeInsightsAnnotation>> annotationChunks = analysisDetails.getPostAnalysisIssueVisitor().getIssues().stream()
.filter(i -> i.getComponent().getReportAttributes().getScmPath().isPresent())
.filter(i -> i.getComponent().getType() == Component.Type.FILE)
.filter(i -> OPEN_ISSUE_STATUSES.contains(i.getIssue().status()))
.sorted(Comparator.comparing(a -> Severity.ALL.indexOf(a.getIssue().severity())))
.map(componentIssue -> {
String path = componentIssue.getComponent().getReportAttributes().getScmPath().get();
return new Annotation(componentIssue.getIssue().key(),
return client.createCodeInsightsAnnotation(componentIssue.getIssue().key(),
Optional.ofNullable(componentIssue.getIssue().getLine()).orElse(0),
analysisDetails.getIssueUrl(componentIssue.getIssue().key()),
componentIssue.getIssue().getMessage(),
@ -142,9 +148,9 @@ public class BitbucketServerPullRequestDecorator implements PullRequestBuildStat
toBitbucketType(componentIssue.getIssue().type()));
}).collect(Collectors.groupingBy(s -> chunkCounter.getAndIncrement() / DEFAULT_MAX_ANNOTATIONS, toSet()));
for (Set<Annotation> annotations : annotationChunks.values()) {
for (Set<CodeInsightsAnnotation> annotations : annotationChunks.values()) {
try {
client.createAnnotations(project, repo, analysisDetails.getCommitSha(), new CreateAnnotationsRequest(annotations), almSettingDto);
client.uploadAnnotations(project, repo, analysisDetails.getCommitSha(), annotations);
} catch (BitbucketException e) {
if (e.isError(BitbucketException.PAYLOAD_TOO_LARGE)) {
LOGGER.warn("The annotations will be truncated since the maximum number of annotations for this report has been reached.");
@ -156,10 +162,6 @@ public class BitbucketServerPullRequestDecorator implements PullRequestBuildStat
}
}
private String asInsightStatus(QualityGate.Status status) {
return QualityGate.Status.ERROR.equals(status) ? "FAIL" : "PASS";
}
private String toBitbucketSeverity(String severity) {
if (severity == null) {
return "LOW";

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Mathias Åhsberg
* Copyright (C) 2020 Marvin Wichmann
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@ -18,137 +18,75 @@
*/
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CreateAnnotationsRequest;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CreateReportRequest;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.ErrorResponse;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.ServerProperties;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.db.alm.setting.AlmSettingDto;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CodeInsightsAnnotation;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.DataValue;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.ReportData;
import org.sonar.api.ce.posttask.QualityGate;
import java.io.IOException;
import java.util.Optional;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import static java.lang.String.format;
public interface BitbucketClient {
@ComputeEngineSide
public class BitbucketClient {
private static final Logger LOGGER = Loggers.get(BitbucketClient.class);
private static final String REPORT_KEY = "com.github.mc1arke.sonarqube";
private static final MediaType APPLICATION_JSON_MEDIA_TYPE = MediaType.get("application/json");
/**
* <p>
* Creates an annotation for the given parameters based on the fact if the cloud
* or the on prem bitbucket solution is used.
* </p>
*
* @return The newly created {@link CodeInsightsAnnotation}
*/
CodeInsightsAnnotation createCodeInsightsAnnotation(String issueKey, int line, String issueUrl, String message, String path, String severity, String type);
private OkHttpClient client;
private ObjectMapper objectMapper;
/**
* <p>
* Creates a report for the given parameters based on the fact if the cloud
* or the on prem bitbucket solution is used.
* </p>
*
* @return The newly created {@link CodeInsightsReport}
*/
CodeInsightsReport createCodeInsightsReport(List<ReportData> reportData,
String reportDescription, Instant creationDate, String dashboardUrl,
String logoUrl, QualityGate.Status status);
/**
* Deletes all code insights annotations for the given parameters.
*
* @throws IOException if the annotations cannot be deleted
*/
void deleteAnnotations(String project, String repo, String commitSha) throws IOException;
public ServerProperties getServerProperties(AlmSettingDto almSettingDto) throws IOException {
Request req = new Request.Builder()
.get()
.url(format("%s/rest/api/1.0/application-properties", almSettingDto.getUrl()))
.build();
try (Response response = getClient(almSettingDto).newCall(req).execute()) {
validate(response);
/**
* Uploads CodeInsights Annotations for the given commit.
*
* @throws IOException if the annotations cannot be uploaded
*/
void uploadAnnotations(String project, String repo, String commitSha, Set<CodeInsightsAnnotation> annotations) throws IOException;
return getObjectMapper().reader().forType(ServerProperties.class)
.readValue(Optional.ofNullable(response.body())
.orElseThrow(() -> new IllegalStateException("No response body from BitBucket"))
.string());
}
}
/**
* Creates a DataValue of type DataValue.Link or DataValue.CloudLink depending on the implementation
*/
DataValue createLinkDataValue(String dashboardUrl);
public void createReport(String project, String repository, String commit, CreateReportRequest request, AlmSettingDto almSettingDto) throws IOException {
String body = getObjectMapper().writeValueAsString(request);
Request req = new Request.Builder()
.put(RequestBody.create(APPLICATION_JSON_MEDIA_TYPE, body))
.url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s", almSettingDto.getUrl(), project, repository, commit, REPORT_KEY))
.build();
/**
* Uploads the code insights report for the given commit
*/
void uploadReport(String project, String repo, String commitSha, CodeInsightsReport codeInsightReport) throws IOException;
try (Response response = getClient(almSettingDto).newCall(req).execute()) {
validate(response);
}
}
/**
* <p>
* Determines if the used bitbucket endpoint supports the code insights feature.
* <p>
* For the cloud version we simply return true and for the server version a version
* check is implemented that tests if the given server version is higher than 5.15
* </p>
*
* @return boolean
*/
boolean supportsCodeInsights();
public void createAnnotations(String project, String repository, String commit, CreateAnnotationsRequest request, AlmSettingDto almSettingDto) throws IOException {
if (request.getAnnotations().isEmpty()) {
return;
}
Request req = new Request.Builder()
.post(RequestBody.create(APPLICATION_JSON_MEDIA_TYPE, getObjectMapper().writeValueAsString(request)))
.url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s/annotations", almSettingDto.getUrl(), project, repository, commit, REPORT_KEY))
.build();
try (Response response = getClient(almSettingDto).newCall(req).execute()) {
validate(response);
}
}
public void deleteAnnotations(String project, String repository, String commit, AlmSettingDto almSettingDto) throws IOException {
Request req = new Request.Builder()
.delete()
.url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s/annotations", almSettingDto.getUrl(), project, repository, commit, REPORT_KEY))
.build();
try (Response response = getClient(almSettingDto).newCall(req).execute()) {
validate(response);
}
}
public boolean supportsCodeInsights(AlmSettingDto almSettingDto) {
try {
ServerProperties server = getServerProperties(almSettingDto);
LOGGER.debug(format("Your Bitbucket Server installation is version %s", server.getVersion()));
if (server.hasCodeInsightsApi()) {
return true;
} else {
LOGGER.info("Bitbucket Server version is to old. %s is the minimum version that supports Code Insights",
ServerProperties.CODE_INSIGHT_VERSION);
}
} catch (IOException e) {
LOGGER.error("Could not determine Bitbucket Server version", e);
return false;
}
return false;
}
private void validate(Response response) throws IOException {
LOGGER.debug(format("Validate response %s", response));
if (!response.isSuccessful()) {
ErrorResponse errors = null;
if (response.body() != null) {
errors = getObjectMapper().reader().forType(ErrorResponse.class)
.readValue(response.body().string());
}
throw new BitbucketException(response.code(), errors);
}
}
private OkHttpClient getClient(AlmSettingDto almSettingDto) {
client = Optional.ofNullable(client).orElseGet(() ->
new OkHttpClient.Builder()
.authenticator(((route, response) ->
response.request()
.newBuilder()
.header("Authorization", format("Bearer %s", almSettingDto.getPersonalAccessToken()))
.header("Accept", APPLICATION_JSON_MEDIA_TYPE.toString())
.build()
))
.build()
);
return client;
}
private ObjectMapper getObjectMapper() {
objectMapper = Optional.ofNullable(objectMapper).orElseGet(() -> new ObjectMapper()
.setSerializationInclusion(Include.NON_NULL)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
);
return objectMapper;
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2020 Marvin Wichmann
*
* 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.BitbucketConfiguration;
public final class BitbucketClientFactory {
private BitbucketClientFactory() {
}
public static BitbucketClient createClient(BitbucketConfiguration bitbucketConfiguration) {
if (bitbucketConfiguration.isCloud()) {
return new BitbucketCloudClient(bitbucketConfiguration, createObjectMapper());
} else {
return new BitbucketServerClient(bitbucketConfiguration, createObjectMapper());
}
}
private static ObjectMapper createObjectMapper() {
return new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
}

View File

@ -0,0 +1,185 @@
/*
* Copyright (C) 2020 Marvin Wichmann
*
* 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.BitbucketConfiguration;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CodeInsightsAnnotation;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.DataValue;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.ReportData;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.cloud.CloudAnnotation;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.cloud.CloudCreateReportRequest;
import com.google.common.annotations.VisibleForTesting;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.sonar.api.ce.posttask.QualityGate;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import java.io.IOException;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static java.lang.String.format;
public class BitbucketCloudClient implements BitbucketClient {
private static final Logger LOGGER = Loggers.get(BitbucketCloudClient.class);
private static final String REPORT_KEY = "com.github.mc1arke.sonarqube";
private static final MediaType APPLICATION_JSON_MEDIA_TYPE = MediaType.get("application/json");
private static final String TITLE = "SonarQube";
private static final String REPORTER = "SonarQube";
private static final String LINK_TEXT = "Go to SonarQube";
private final BitbucketConfiguration config;
private final ObjectMapper objectMapper;
public BitbucketCloudClient(BitbucketConfiguration config, ObjectMapper objectMapper) {
this.config = config;
this.objectMapper = objectMapper;
}
@Override
public CodeInsightsAnnotation createCodeInsightsAnnotation(String issueKey, int line, String issueUrl, String message,
String path, String severity, String type) {
return new CloudAnnotation(issueKey,
line,
issueUrl,
message,
path,
severity,
type);
}
@Override
public CodeInsightsReport createCodeInsightsReport(List<ReportData> reportData, String reportDescription,
Instant creationDate, String dashboardUrl, String logoUrl,
QualityGate.Status status) {
return new CloudCreateReportRequest(
reportData,
reportDescription,
TITLE,
REPORTER,
Date.from(creationDate),
dashboardUrl, // you need to change this to a real https URL for local debugging since localhost will get declined by the API
logoUrl,
"COVERAGE",
QualityGate.Status.ERROR.equals(status) ? "FAILED" : "PASSED"
);
}
@Override
public void deleteAnnotations(String project, String repo, String commitSha) throws IOException {
// not needed here.
}
public void uploadAnnotations(String project, String repository, String commit, Set<CodeInsightsAnnotation> baseAnnotations) throws IOException {
Set<CloudAnnotation> annotations = baseAnnotations.stream().map(annotation -> (CloudAnnotation) annotation).collect(Collectors.toSet());
if (annotations.isEmpty()) {
return;
}
Request req = new Request.Builder()
.post(RequestBody.create(APPLICATION_JSON_MEDIA_TYPE, objectMapper.writeValueAsString(annotations)))
.url(format("%s/2.0/repositories/%s/%s/commit/%s/reports/%s/annotations", config.getUrl(), project, repository, commit, REPORT_KEY))
.build();
LOGGER.info("Creating annotations on bitbucket cloud");
LOGGER.debug("Create annotations: " + objectMapper.writeValueAsString(annotations));
try (Response response = getClient().newCall(req).execute()) {
validate(response);
}
}
@Override
public DataValue createLinkDataValue(String dashboardUrl) {
return new DataValue.CloudLink(LINK_TEXT, dashboardUrl);
}
@Override
public void uploadReport(String project, String repository, String commit, CodeInsightsReport codeInsightReport) throws IOException {
deleteExistingReport(project, repository, commit);
String body = objectMapper.writeValueAsString(codeInsightReport);
Request req = new Request.Builder()
.put(RequestBody.create(APPLICATION_JSON_MEDIA_TYPE, body))
.url(format("%s/2.0/repositories/%s/%s/commit/%s/reports/%s", config.getUrl(), project, repository, commit, REPORT_KEY))
.build();
LOGGER.info("Create report on bitbucket cloud");
LOGGER.debug("Create report: " + body);
try (Response response = getClient().newCall(req).execute()) {
validate(response);
}
}
@Override
public boolean supportsCodeInsights() {
return true;
}
void deleteExistingReport(String project, String repository, String commit) throws IOException {
Request req = new Request.Builder()
.delete()
.url(format("%s/2.0/repositories/%s/%s/commit/%s/reports/%s", config.getUrl(), project, repository, commit, REPORT_KEY))
.build();
LOGGER.info("Deleting existing reports on bitbucket cloud");
try (Response response = getClient().newCall(req).execute()) {
// we dont need to validate the output here since most of the time this call will just return a 404
}
}
@VisibleForTesting
OkHttpClient getClient() {
return new OkHttpClient.Builder()
.addInterceptor(chain -> {
Request newRequest = chain.request().newBuilder()
.addHeader("Authorization", format("Basic %s", config.getToken()))
.addHeader("Accept", APPLICATION_JSON_MEDIA_TYPE.toString())
.build();
return chain.proceed(newRequest);
})
.build();
}
void validate(Response response) throws IOException {
if (!response.isSuccessful()) {
String error;
if (response.body() != null) {
error = response.body().string();
} else {
error = "Request failed but Bitbucket didn't respond with a proper error message";
}
throw new BitbucketCloudException(response.code(), error);
}
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (C) 2020 Marvin Wichmann
*
* 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client;
public class BitbucketCloudException extends RuntimeException {
private final int code;
private final String error;
BitbucketCloudException(int code, String error) {
this.code = code;
this.error = error;
}
public boolean isError(int code) {
return this.code == code;
}
@Override
public String getMessage() {
return "HTTP Status Code: " + code + "; Message:" + error;
}
}

View File

@ -18,7 +18,7 @@
*/
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.ErrorResponse;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.server.ErrorResponse;
import java.util.Optional;
import java.util.stream.Collectors;

View File

@ -0,0 +1,192 @@
/*
* Copyright (C) 2020 Mathias Åhsberg
*
* 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.BitbucketConfiguration;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CodeInsightsAnnotation;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.DataValue;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.ReportData;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.server.Annotation;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.server.CreateAnnotationsRequest;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.server.CreateReportRequest;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.server.ErrorResponse;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.server.ServerProperties;
import com.google.common.annotations.VisibleForTesting;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.sonar.api.ce.posttask.QualityGate;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static java.lang.String.format;
public class BitbucketServerClient implements BitbucketClient {
private static final Logger LOGGER = Loggers.get(BitbucketServerClient.class);
private static final String REPORT_KEY = "com.github.mc1arke.sonarqube";
private static final MediaType APPLICATION_JSON_MEDIA_TYPE = MediaType.get("application/json");
private static final String TITLE = "SonarQube";
private static final String REPORTER = "SonarQube";
private static final String LINK_TEXT = "Go to SonarQube";
private final BitbucketConfiguration config;
private final ObjectMapper objectMapper;
public BitbucketServerClient(BitbucketConfiguration config, ObjectMapper objectMapper) {
this.config = config;
this.objectMapper = objectMapper;
}
@Override
public CodeInsightsAnnotation createCodeInsightsAnnotation(String issueKey, int line, String issueUrl, String message, String path, String severity, String type) {
return new Annotation(issueKey,
line,
issueUrl,
message,
path,
severity,
type);
}
@Override
public CodeInsightsReport createCodeInsightsReport(List<ReportData> reportData, String reportDescription, Instant creationDate, String dashboardUrl, String logoUrl, QualityGate.Status status) {
return new CreateReportRequest(
reportData,
reportDescription,
TITLE,
REPORTER,
creationDate,
dashboardUrl,
logoUrl,
QualityGate.Status.ERROR.equals(status) ? "FAIL" : "PASS"
);
}
public void deleteAnnotations(String project, String repository, String commit) throws IOException {
Request req = new Request.Builder()
.delete()
.url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s/annotations", config.getUrl(), project, repository, commit, REPORT_KEY))
.build();
try (Response response = getClient().newCall(req).execute()) {
validate(response);
}
}
@Override
public void uploadAnnotations(String project, String repository, String commit, Set<CodeInsightsAnnotation> annotations) throws IOException {
Set<Annotation> annotationSet = annotations.stream().map(annotation -> (Annotation) annotation).collect(Collectors.toSet());
CreateAnnotationsRequest request = new CreateAnnotationsRequest(annotationSet);
if (request.getAnnotations().isEmpty()) {
return;
}
Request req = new Request.Builder()
.post(RequestBody.create(APPLICATION_JSON_MEDIA_TYPE, objectMapper.writeValueAsString(request)))
.url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s/annotations", config.getUrl(), project, repository, commit, REPORT_KEY))
.build();
try (Response response = getClient().newCall(req).execute()) {
validate(response);
}
}
@Override
public DataValue createLinkDataValue(String dashboardUrl) {
return new DataValue.Link(LINK_TEXT, dashboardUrl);
}
@Override
public void uploadReport(String project, String repository, String commit, CodeInsightsReport codeInsightReport) throws IOException {
String body = objectMapper.writeValueAsString(codeInsightReport);
Request req = new Request.Builder()
.put(RequestBody.create(APPLICATION_JSON_MEDIA_TYPE, body))
.url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s", config.getUrl(), project, repository, commit, REPORT_KEY))
.build();
try (Response response = getClient().newCall(req).execute()) {
validate(response);
}
}
@Override
public boolean supportsCodeInsights() {
try {
ServerProperties server = getServerProperties();
LOGGER.debug(format("Your Bitbucket Server installation is version %s", server.getVersion()));
if (server.hasCodeInsightsApi()) {
return true;
} else {
LOGGER.info("Bitbucket Server version is to old. %s is the minimum version that supports Code Insights",
ServerProperties.CODE_INSIGHT_VERSION);
}
} catch (IOException e) {
LOGGER.error("Could not determine Bitbucket Server version", e);
return false;
}
return false;
}
public ServerProperties getServerProperties() throws IOException {
Request req = new Request.Builder()
.get()
.url(format("%s/rest/api/1.0/application-properties", config.getUrl()))
.build();
try (Response response = getClient().newCall(req).execute()) {
validate(response);
return objectMapper.reader().forType(ServerProperties.class)
.readValue(Optional.ofNullable(response.body())
.orElseThrow(() -> new IllegalStateException("No response body from BitBucket"))
.string());
}
}
@VisibleForTesting
OkHttpClient getClient() {
return new OkHttpClient.Builder()
.addInterceptor(chain -> {
Request newRequest = chain.request().newBuilder()
.addHeader("Authorization", format("Bearer %s", config.getToken()))
.addHeader("Accept", APPLICATION_JSON_MEDIA_TYPE.toString())
.build();
return chain.proceed(newRequest);
})
.build();
}
void validate(Response response) throws IOException {
if (!response.isSuccessful()) {
ErrorResponse errors = null;
if (response.body() != null) {
errors = objectMapper.reader().forType(ErrorResponse.class)
.readValue(response.body().string());
}
throw new BitbucketException(response.code(), errors);
}
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright (C) 2020 Marvin Wichmann
*
* 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model;
import java.util.Locale;
public class BitbucketConfiguration {
private final String url;
private final String token;
private final String repository;
private final String project;
public BitbucketConfiguration(String url, String token, String repository, String project) {
this.url = url;
this.token = token;
this.repository = repository;
this.project = project;
}
public String getRepository() {
return repository;
}
public String getToken() {
return token;
}
public String getUrl() {
return url;
}
public String getProject() {
return project;
}
public boolean isCloud() {
return url.toLowerCase(Locale.ENGLISH).startsWith("https://api.bitbucket.org");
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright (C) 2020 Marvin Wichmann
*
* 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Class for reusing models between the cloud and the server version
*/
public class CodeInsightsAnnotation {
@JsonProperty("line")
private final int line;
@JsonProperty("message")
private final String message;
@JsonProperty("path")
private final String path;
@JsonProperty("severity")
private final String severity;
public CodeInsightsAnnotation(int line, String message, String path, String severity) {
this.line = line;
this.message = message;
this.path = path;
this.severity = severity;
}
public int getLine() {
return line;
}
public String getMessage() {
return message;
}
public String getPath() {
return path;
}
public String getSeverity() {
return severity;
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright (C) 2020 Marvin Wichmann
*
* 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
/**
* Interface for reusing models between the cloud and the server version
*/
public class CodeInsightsReport {
@JsonProperty("data")
private final List<ReportData> data;
@JsonProperty("details")
private final String details;
@JsonProperty("title")
private final String title;
@JsonProperty("reporter")
private final String reporter;
@JsonProperty("link")
private final String link;
@JsonProperty("result")
private final String result;
public CodeInsightsReport(List<ReportData> data, String details, String title, String reporter, String link, String result) {
this.data = data;
this.details = details;
this.title = title;
this.reporter = reporter;
this.link = link;
this.result = result;
}
public List<ReportData> getData() {
return data;
}
public String getDetails() {
return details;
}
public String getTitle() {
return title;
}
public String getReporter() {
return reporter;
}
public String getLink() {
return link;
}
public String getResult() {
return result;
}
}

View File

@ -46,6 +46,25 @@ public interface DataValue extends Serializable {
}
}
class CloudLink implements DataValue {
private final String text;
private final String href;
@JsonCreator
public CloudLink(@JsonProperty("text") String text, @JsonProperty("href") String href) {
this.text = text;
this.href = href;
}
public String getText() {
return text;
}
public String getHref() {
return href;
}
}
class Text implements DataValue {
private final String value;

View File

@ -34,6 +34,16 @@ public class ReportData {
this.type = typeFrom(value);
}
private static String typeFrom(DataValue value) {
if (value instanceof DataValue.Link || value instanceof DataValue.CloudLink) {
return "LINK";
} else if (value instanceof DataValue.Percentage) {
return "PERCENTAGE";
} else {
return "TEXT";
}
}
public String getTitle() {
return title;
}
@ -45,14 +55,4 @@ public class ReportData {
public String getType() {
return type;
}
private static String typeFrom(DataValue value) {
if (value instanceof DataValue.Link) {
return "LINK";
} else if (value instanceof DataValue.Percentage) {
return "PERCENTAGE";
} else {
return "TEXT";
}
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright (C) 2020 Marvin Wichmann
*
* 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.cloud;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CodeInsightsAnnotation;
public class CloudAnnotation extends CodeInsightsAnnotation {
@JsonProperty("external_id")
private final String externalId;
@JsonProperty("summary")
private final String link;
@JsonProperty("annotation_type")
private final String annotationType;
@JsonCreator
public CloudAnnotation(String externalId,
int line,
String link,
String message,
String path,
String severity,
String annotationType) {
super(line, message, path, severity);
this.externalId = externalId;
this.link = link;
this.annotationType = annotationType;
}
public String getExternalId() {
return externalId;
}
public String getLink() {
return link;
}
public String getAnnotationType() {
return annotationType;
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright (C) 2020 Marvin Wichmann
*
* 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 com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.cloud;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.ReportData;
import java.util.Date;
import java.util.List;
public class CloudCreateReportRequest extends CodeInsightsReport {
@JsonProperty("created_on")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssZ")
private final Date createdDate;
@JsonProperty("logo_url")
private final String logoUrl;
@JsonProperty("report_type")
private final String reportType;
@JsonProperty("remote_link_enabled")
private final boolean remoteLinkEnabled;
@JsonCreator
public CloudCreateReportRequest(
List<ReportData> data,
String details,
String title,
String reporter,
Date createdDate,
String link,
String logoUrl,
String reportType,
String result) {
super(data, details, title, reporter, link, result);
this.createdDate = createdDate;
this.logoUrl = logoUrl;
this.reportType = reportType;
this.remoteLinkEnabled = true;
}
public Date getCreatedDate() {
return createdDate;
}
public String getLogoUrl() {
return logoUrl;
}
public String getReportType() {
return reportType;
}
public Boolean getRemoteLinkEnabled() {
return remoteLinkEnabled;
}
}

View File

@ -16,36 +16,30 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model;
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.server;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CodeInsightsAnnotation;
import java.io.Serializable;
public class Annotation implements Serializable {
public class Annotation extends CodeInsightsAnnotation {
private final String externalId;
private final int line;
private final String link;
private final String message;
private final String path;
private final String severity;
private final String type;
@JsonCreator
public Annotation(@JsonProperty("externalId") String externalId,
@JsonProperty("line") int line,
@JsonProperty("link") String link,
@JsonProperty("message") String message,
@JsonProperty("path") String path,
@JsonProperty("severity") String severity,
String link,
String message,
String path,
String severity,
@JsonProperty("type") String type) {
super(line, message, path, severity);
this.externalId = externalId;
this.line = line;
this.link = link;
this.message = message;
this.path = path;
this.severity = severity;
this.type = type;
}
@ -53,26 +47,10 @@ public class Annotation implements Serializable {
return externalId;
}
public int getLine() {
return line;
}
public String getLink() {
return link;
}
public String getMessage() {
return message;
}
public String getPath() {
return path;
}
public String getSeverity() {
return severity;
}
public String getType() {
return type;
}

View File

@ -16,7 +16,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model;
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.server;
import java.io.Serializable;
import java.util.Collections;

View File

@ -16,23 +16,20 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model;
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.server;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.ReportData;
import java.time.Instant;
import java.util.List;
public class CreateReportRequest {
private final List<ReportData> data;
private final String details;
private final String title;
private final String reporter;
public class CreateReportRequest extends CodeInsightsReport {
private final Instant createdDate;
private final String link;
private final String logoUrl;
private final String result;
@JsonCreator
public CreateReportRequest(
@ -44,45 +41,16 @@ public class CreateReportRequest {
@JsonProperty("link") String link,
@JsonProperty("logoUrl") String logoUrl,
@JsonProperty("result") String result) {
this.data = data;
this.details = details;
this.title = title;
this.reporter = reporter;
super(data, details, title, reporter, link, result);
this.createdDate = createdDate;
this.link = link;
this.logoUrl = logoUrl;
this.result = result;
}
public List<ReportData> getData() {
return data;
}
public String getDetails() {
return details;
}
public String getTitle() {
return title;
}
public String getReporter() {
return reporter;
}
public Instant getCreatedDate() {
return createdDate;
}
public String getLink() {
return link;
}
public String getLogoUrl() {
return logoUrl;
}
public String getResult() {
return result;
}
}

View File

@ -16,7 +16,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model;
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.server;
import com.fasterxml.jackson.annotation.JsonProperty;
@ -27,7 +27,7 @@ import java.util.Set;
public class ErrorResponse implements Serializable {
private final Set<Error> errors;
ErrorResponse(@JsonProperty("errors") Set<Error> errors) {
public ErrorResponse(@JsonProperty("errors") Set<Error> errors) {
this.errors = errors;
}
@ -39,7 +39,7 @@ public class ErrorResponse implements Serializable {
private final String message;
Error(@JsonProperty("message") String message) {
public Error(@JsonProperty("message") String message) {
this.message = message;
}

View File

@ -16,7 +16,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model;
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.server;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

View File

@ -65,7 +65,7 @@ public class CommunityBranchPluginBootstrapTest {
@Test
public void testDefineInvokedOnSuccessLoad() throws ClassNotFoundException {
Plugin.Context context = spy(mock(Plugin.Context.class));
Plugin.Context context = mock(Plugin.Context.class);
Configuration configuration = mock(Configuration.class);
when(context.getBootConfiguration()).thenReturn(configuration);
when(configuration.get(any())).thenReturn(Optional.empty());
@ -92,7 +92,7 @@ public class CommunityBranchPluginBootstrapTest {
@Test
public void testDefineNotInvokedForNonScanner() throws ClassNotFoundException {
Plugin.Context context = spy(mock(Plugin.Context.class));
Plugin.Context context = mock(Plugin.Context.class);
Configuration configuration = mock(Configuration.class);
when(context.getBootConfiguration()).thenReturn(configuration);
when(configuration.get(any())).thenReturn(Optional.empty());

View File

@ -62,7 +62,7 @@ public class CommunityBranchPluginTest {
public void testScannerSideDefine() {
final CommunityBranchPlugin testCase = new CommunityBranchPlugin();
final Plugin.Context context = spy(mock(Plugin.Context.class, Mockito.RETURNS_DEEP_STUBS));
final Plugin.Context context = mock(Plugin.Context.class, Mockito.RETURNS_DEEP_STUBS);
when(context.getRuntime().getSonarQubeSide()).thenReturn(SonarQubeSide.SCANNER);
testCase.define(context);
@ -81,7 +81,7 @@ public class CommunityBranchPluginTest {
public void testNonScannerSideDefine() {
final CommunityBranchPlugin testCase = new CommunityBranchPlugin();
final Plugin.Context context = spy(mock(Plugin.Context.class, Mockito.RETURNS_DEEP_STUBS));
final Plugin.Context context = mock(Plugin.Context.class, Mockito.RETURNS_DEEP_STUBS);
when(context.getRuntime().getSonarQubeSide()).thenReturn(SonarQubeSide.SERVER);
testCase.define(context);
@ -93,7 +93,7 @@ public class CommunityBranchPluginTest {
public void testComputeEngineSideLoad() {
final CommunityBranchPlugin testCase = new CommunityBranchPlugin();
final CoreExtension.Context context = spy(mock(CoreExtension.Context.class, Mockito.RETURNS_DEEP_STUBS));
final CoreExtension.Context context = mock(CoreExtension.Context.class, Mockito.RETURNS_DEEP_STUBS);
when(context.getRuntime().getSonarQubeSide()).thenReturn(SonarQubeSide.COMPUTE_ENGINE);
testCase.load(context);

View File

@ -32,7 +32,7 @@ public class CommunityReportAnalysisComponentProviderTest {
@Test
public void testGetComponents() {
List<Object> result = new CommunityReportAnalysisComponentProvider().getComponents();
assertEquals(10, result.size());
assertEquals(9, result.size());
assertEquals(CommunityBranchLoaderDelegate.class, result.get(0));
}
}

View File

@ -3,13 +3,10 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.BitbucketClient;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.Annotation;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CreateAnnotationsRequest;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CreateReportRequest;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.BitbucketConfiguration;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.junit.MockitoJUnitRunner;
import org.sonar.api.ce.posttask.QualityGate;
import org.sonar.api.issue.Issue;
@ -24,14 +21,13 @@ import org.sonar.db.alm.setting.ProjectAlmSettingDto;
import java.io.IOException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@ -56,7 +52,12 @@ public class BitbucketPullRequestDecoratorTest {
private BitbucketClient client = mock(BitbucketClient.class);
private BitbucketServerPullRequestDecorator underTest = new BitbucketServerPullRequestDecorator(client);
private BitbucketPullRequestDecorator underTest = new BitbucketPullRequestDecorator() {
@Override
BitbucketClient createClient(BitbucketConfiguration bitbucketConfiguration) {
return client;
}
};
private final AlmSettingDto almSettingDto = mock(AlmSettingDto.class);
private final ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class);
@ -69,23 +70,15 @@ public class BitbucketPullRequestDecoratorTest {
@Test
public void testValidAnalysis() throws IOException {
when(client.supportsCodeInsights(eq(almSettingDto))).thenReturn(true);
when(client.supportsCodeInsights()).thenReturn(true);
mockValidAnalysis();
final ArgumentCaptor<CreateReportRequest> reportCaptor = ArgumentCaptor.forClass(CreateReportRequest.class);
final ArgumentCaptor<CreateAnnotationsRequest> annotationsCaptor = ArgumentCaptor.forClass(CreateAnnotationsRequest.class);
underTest.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto);
verify(client).createReport(eq(PROJECT), eq(REPO), eq(COMMIT), reportCaptor.capture(), eq(almSettingDto));
verifyExpectedReport(reportCaptor.getValue());
verify(client).deleteAnnotations(PROJECT, REPO, COMMIT, almSettingDto);
verify(client).createAnnotations(eq(PROJECT), eq(REPO), eq(COMMIT), annotationsCaptor.capture(), eq(almSettingDto));
CreateAnnotationsRequest actualAnnotations = annotationsCaptor.getValue();
assertThat(actualAnnotations.getAnnotations()).size().isEqualTo(1);
verifyExpectedAnnotation(actualAnnotations.getAnnotations().iterator().next());
verify(client).createCodeInsightsAnnotation(eq(ISSUE_KEY), eq(ISSUE_LINE), eq(ISSUE_LINK), eq(ISSUE_MESSAGE), eq(ISSUE_PATH), eq("HIGH"), eq("BUG"));
verify(client).createLinkDataValue(DASHBOARD_URL);
verify(client).createCodeInsightsReport(any(), eq("Quality Gate passed" + System.lineSeparator()), any(), eq(DASHBOARD_URL), eq(String.format("%s/common/icon.png", IMAGE_URL)), eq(QualityGate.Status.OK));
verify(client).deleteAnnotations(PROJECT, REPO, COMMIT);
}
private void mockValidAnalysis() {
@ -131,25 +124,4 @@ public class BitbucketPullRequestDecoratorTest {
when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(postAnalysisIssueVisitor);
}
private void verifyExpectedReport(CreateReportRequest actual) {
assertThat(actual.getTitle()).isEqualTo("SonarQube");
assertThat(actual.getResult()).isEqualTo("PASS");
assertThat(actual.getReporter()).isEqualTo("SonarQube");
assertThat(actual.getCreatedDate()).isBetween(Instant.now().minus(1, ChronoUnit.MINUTES), Instant.now());
assertThat(actual.getDetails()).isEqualTo("Quality Gate passed" + System.lineSeparator());
assertThat(actual.getLink()).isEqualTo(DASHBOARD_URL);
assertThat(actual.getLogoUrl()).isEqualTo(String.format("%s/common/icon.png", IMAGE_URL));
assertThat(actual.getData()).size().isEqualTo(6);
}
private void verifyExpectedAnnotation(Annotation actual) {
assertThat(actual.getExternalId()).isEqualTo(ISSUE_KEY);
assertThat(actual.getLine()).isEqualTo(ISSUE_LINE);
assertThat(actual.getLink()).isEqualTo(ISSUE_LINK);
assertThat(actual.getMessage()).isEqualTo(ISSUE_MESSAGE);
assertThat(actual.getPath()).isEqualTo(ISSUE_PATH);
assertThat(actual.getSeverity()).isEqualTo("HIGH");
assertThat(actual.getType()).isEqualTo("BUG");
}
}

View File

@ -0,0 +1,35 @@
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.BitbucketConfiguration;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
public class BitbucketClientFactoryUnitTest {
@Test
public void testCreateClientIsCloudIfUrlMatches() {
// given
BitbucketConfiguration configuration = new BitbucketConfiguration("https://api.bitbucket.org", "token", "repository", "project");
// when
BitbucketClient client = BitbucketClientFactory.createClient(configuration);
// then
assertTrue(client instanceof BitbucketCloudClient);
}
@Test
public void testCreateClientIsServerIfNotApiUrl() {
// given
BitbucketConfiguration configuration = new BitbucketConfiguration("https://api.server.org", "token", "repository", "project");
// when
BitbucketClient client = BitbucketClientFactory.createClient(configuration);
// then
assertTrue(client instanceof BitbucketServerClient);
}
}

View File

@ -0,0 +1,223 @@
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.BitbucketConfiguration;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CodeInsightsAnnotation;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.DataValue;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.cloud.CloudAnnotation;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.cloud.CloudCreateReportRequest;
import com.google.common.collect.Sets;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.sonar.api.ce.posttask.QualityGate;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.internal.verification.VerificationModeFactory.times;
@RunWith(MockitoJUnitRunner.class)
public class BitbucketCloudClientUnitTest {
private BitbucketCloudClient underTest;
@Mock
private ObjectMapper mapper;
@Mock
private OkHttpClient client;
@Before
public void before() {
BitbucketConfiguration config = new BitbucketConfiguration("https://api.bitbucket.org", "token", "repository", "project");
underTest = new BitbucketCloudClient(config, mapper) {
@Override
OkHttpClient getClient() {
return client;
}
};
}
@Test
public void testUploadReport() throws IOException {
// given
CodeInsightsReport report = mock(CodeInsightsReport.class);
Call call = mock(Call.class);
Response response = mock(Response.class);
ArgumentCaptor<Request> captor = ArgumentCaptor.forClass(Request.class);
when(client.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(response);
when(response.isSuccessful()).thenReturn(true);
when(mapper.writeValueAsString(report)).thenReturn("{payload}");
// when
underTest.uploadReport("project", "repository", "commit", report);
// then
verify(client, times(2)).newCall(captor.capture());
Request request = captor.getValue();
assertEquals("PUT", request.method());
assertEquals("https://api.bitbucket.org/2.0/repositories/project/repository/commit/commit/reports/com.github.mc1arke.sonarqube", request.url().toString());
}
@Test
public void testDeleteReport() throws IOException {
// given
Call call = mock(Call.class);
Response response = mock(Response.class);
ArgumentCaptor<Request> captor = ArgumentCaptor.forClass(Request.class);
when(client.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(response);
// when
underTest.deleteExistingReport("project", "repository", "commit");
// then
verify(client, times(1)).newCall(captor.capture());
Request request = captor.getValue();
assertEquals("DELETE", request.method());
assertEquals("https://api.bitbucket.org/2.0/repositories/project/repository/commit/commit/reports/com.github.mc1arke.sonarqube", request.url().toString());
}
@Test
public void testUploadAnnotations() throws IOException {
// given
CodeInsightsAnnotation annotation = mock(CloudAnnotation.class);
Set<CodeInsightsAnnotation> annotations = Sets.newHashSet(annotation);
Call call = mock(Call.class);
Response response = mock(Response.class);
ArgumentCaptor<Request> captor = ArgumentCaptor.forClass(Request.class);
when(client.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(response);
when(response.isSuccessful()).thenReturn(true);
when(mapper.writeValueAsString(any())).thenReturn("{payload}");
// when
underTest.uploadAnnotations("project", "repository", "commit", annotations);
// then
verify(client, times(1)).newCall(captor.capture());
Request request = captor.getValue();
assertEquals("POST", request.method());
assertEquals("https://api.bitbucket.org/2.0/repositories/project/repository/commit/commit/reports/com.github.mc1arke.sonarqube/annotations", request.url().toString());
}
@Test
public void testUploadReportFailsWithMessage() throws IOException {
// given
CodeInsightsReport report = mock(CodeInsightsReport.class);
Call call = mock(Call.class);
Response response = mock(Response.class);
ResponseBody responseBody = mock(ResponseBody.class);
when(client.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(response);
when(response.isSuccessful()).thenReturn(false);
when(response.body()).thenReturn(responseBody);
when(responseBody.string()).thenReturn("error!");
when(response.code()).thenReturn(400);
when(mapper.writeValueAsString(report)).thenReturn("{payload}");
// when,then
assertThatThrownBy(() -> underTest.uploadReport("project", "repository", "commit", report))
.isInstanceOf(BitbucketCloudException.class)
.hasMessage("HTTP Status Code: 400; Message:error!")
.extracting(e -> ((BitbucketCloudException) e).isError(400))
.isEqualTo(true);
}
@Test
public void testUploadAnnotationsWithEmptyAnnotations() throws IOException {
// given
Set<CodeInsightsAnnotation> annotations = Sets.newHashSet();
// when
underTest.uploadAnnotations("project", "repository", "commit", annotations);
// then
verify(client, times(0)).newCall(any());
}
@Test
public void testCreateAnnotationForCloud() {
// given
// when
CodeInsightsAnnotation annotation = underTest.createCodeInsightsAnnotation("issueKey", 12, "http://localhost:9000/dashboard", "Failed", "/path/to/file", "MAJOR", "BUG");
// then
assertTrue(annotation instanceof CloudAnnotation);
assertEquals("issueKey", ((CloudAnnotation) annotation).getExternalId());
assertEquals(12, ((CloudAnnotation) annotation).getLine());
assertEquals("http://localhost:9000/dashboard", ((CloudAnnotation) annotation).getLink());
assertEquals("/path/to/file", ((CloudAnnotation) annotation).getPath());
assertEquals("MAJOR", ((CloudAnnotation) annotation).getSeverity());
assertEquals("BUG", ((CloudAnnotation) annotation).getAnnotationType());
}
@Test
public void testCreateDataLinkForCloud() {
// given
// when
DataValue data = underTest.createLinkDataValue("https://localhost:9000/any/project");
// then
assertTrue(data instanceof DataValue.CloudLink);
assertEquals("https://localhost:9000/any/project", ((DataValue.CloudLink) data).getHref());
}
@Test
public void testCloudAlwaysSupportsCodeInsights() {
// given
// when
boolean result = underTest.supportsCodeInsights();
// then
assertTrue(result);
}
@Test
public void testCreateCloudReport() {
// given
// when
CodeInsightsReport result = underTest.createCodeInsightsReport(new ArrayList<>(), "reportDescription", Instant.now(), "dashboardUrl", "logoUrl", QualityGate.Status.ERROR);
// then
assertTrue(result instanceof CloudCreateReportRequest);
assertEquals(0, ((CloudCreateReportRequest) result).getData().size());
assertEquals("reportDescription", ((CloudCreateReportRequest) result).getDetails());
assertEquals("dashboardUrl", ((CloudCreateReportRequest) result).getLink());
assertEquals("logoUrl", ((CloudCreateReportRequest) result).getLogoUrl());
assertEquals("FAILED", ((CloudCreateReportRequest) result).getResult());
}
}

View File

@ -0,0 +1,388 @@
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.BitbucketConfiguration;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CodeInsightsAnnotation;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.CodeInsightsReport;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.DataValue;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.server.Annotation;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.server.CreateReportRequest;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.server.ErrorResponse;
import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.server.ServerProperties;
import com.google.common.collect.Sets;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.sonar.api.ce.posttask.QualityGate;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.internal.verification.VerificationModeFactory.times;
@RunWith(MockitoJUnitRunner.class)
public class BitbucketServerClientUnitTest {
private BitbucketServerClient underTest;
@Mock
private ObjectMapper mapper;
@Mock
private OkHttpClient client;
@Before
public void before() {
BitbucketConfiguration config = new BitbucketConfiguration("https://my-server.org", "token", "repository", "project");
underTest = new BitbucketServerClient(config, mapper) {
@Override
OkHttpClient getClient() {
return client;
}
};
}
@Test
public void testSupportsCodeInsightsIsFalse() throws IOException {
// given
ServerProperties serverProperties = new ServerProperties("5.0");
Call call = mock(Call.class);
Response response = mock(Response.class);
ObjectReader reader = mock(ObjectReader.class);
ResponseBody responseBody = mock(ResponseBody.class);
when(client.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(response);
when(response.isSuccessful()).thenReturn(true);
when(response.body()).thenReturn(responseBody);
when(responseBody.string()).thenReturn("test");
when(mapper.reader()).thenReturn(reader);
when(reader.forType(ServerProperties.class)).thenReturn(reader);
when(reader.readValue(any(String.class))).thenReturn(serverProperties);
// when
boolean result = underTest.supportsCodeInsights();
// then
assertFalse(result);
}
@Test
public void testSupportsCodeInsightsIsTrueWhenVersionEqual() throws IOException {
// given
ServerProperties serverProperties = new ServerProperties("5.15");
Call call = mock(Call.class);
Response response = mock(Response.class);
ObjectReader reader = mock(ObjectReader.class);
ResponseBody responseBody = mock(ResponseBody.class);
when(client.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(response);
when(response.isSuccessful()).thenReturn(true);
when(response.body()).thenReturn(responseBody);
when(responseBody.string()).thenReturn("test");
when(mapper.reader()).thenReturn(reader);
when(reader.forType(ServerProperties.class)).thenReturn(reader);
when(reader.readValue(any(String.class))).thenReturn(serverProperties);
// when
boolean result = underTest.supportsCodeInsights();
// then
assertTrue(result);
}
@Test
public void testSupportsCodeInsightsIsTrueIfVersionIsHigher() throws IOException {
// given
ServerProperties serverProperties = new ServerProperties("6.0");
Call call = mock(Call.class);
Response response = mock(Response.class);
ObjectReader reader = mock(ObjectReader.class);
ResponseBody responseBody = mock(ResponseBody.class);
when(client.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(response);
when(response.isSuccessful()).thenReturn(true);
when(response.body()).thenReturn(responseBody);
when(responseBody.string()).thenReturn("test");
when(mapper.reader()).thenReturn(reader);
when(reader.forType(ServerProperties.class)).thenReturn(reader);
when(reader.readValue(any(String.class))).thenReturn(serverProperties);
// when
boolean result = underTest.supportsCodeInsights();
// then
assertTrue(result);
}
@Test
public void testSupportsCodeInsightsIsFalseWhenException() throws IOException {
// given
Call call = mock(Call.class);
when(client.newCall(any())).thenReturn(call);
when(call.execute()).thenThrow(new IOException());
// when
boolean result = underTest.supportsCodeInsights();
// then
assertFalse(result);
}
@Test
public void testGetServerProperties() throws IOException {
// given
ServerProperties serverProperties = new ServerProperties("5.0");
Call call = mock(Call.class);
Response response = mock(Response.class);
ObjectReader reader = mock(ObjectReader.class);
ResponseBody responseBody = mock(ResponseBody.class);
ArgumentCaptor<Request> captor = ArgumentCaptor.forClass(Request.class);
when(client.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(response);
when(response.isSuccessful()).thenReturn(true);
when(response.body()).thenReturn(responseBody);
when(responseBody.string()).thenReturn("{version: '5.0'}");
when(mapper.reader()).thenReturn(reader);
when(reader.forType(ServerProperties.class)).thenReturn(reader);
when(reader.readValue(any(String.class))).thenReturn(serverProperties);
// when
ServerProperties result = underTest.getServerProperties();
// then
verify(client, times(1)).newCall(captor.capture());
Request request = captor.getValue();
assertEquals("GET", request.method());
assertEquals("https://my-server.org/rest/api/1.0/application-properties", request.url().toString());
assertEquals("5.0", result.getVersion());
}
@Test
public void testGetServerPropertiesError() throws IOException {
// given
Call call = mock(Call.class);
Response response = mock(Response.class);
ObjectReader reader = mock(ObjectReader.class);
when(client.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(response);
when(response.isSuccessful()).thenReturn(true);
when(response.body()).thenReturn(null);
when(mapper.reader()).thenReturn(reader);
when(reader.forType(ServerProperties.class)).thenReturn(reader);
// when, then
assertThatThrownBy(() -> underTest.getServerProperties())
.isInstanceOf(IllegalStateException.class);
}
@Test
public void testUploadReport() throws IOException {
// given
CodeInsightsReport report = mock(CodeInsightsReport.class);
Call call = mock(Call.class);
Response response = mock(Response.class);
ArgumentCaptor<Request> captor = ArgumentCaptor.forClass(Request.class);
when(client.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(response);
when(response.isSuccessful()).thenReturn(true);
when(mapper.writeValueAsString(report)).thenReturn("{payload}");
// when
underTest.uploadReport("project", "repository", "commit", report);
// then
verify(client, times(1)).newCall(captor.capture());
Request request = captor.getValue();
assertEquals("PUT", request.method());
assertEquals("https://my-server.org/rest/insights/1.0/projects/project/repos/repository/commits/commit/reports/com.github.mc1arke.sonarqube", request.url().toString());
}
@Test
public void testUploadReportFails() throws IOException {
// given
CodeInsightsReport report = mock(CodeInsightsReport.class);
Call call = mock(Call.class);
Response response = mock(Response.class);
when(client.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(response);
when(response.isSuccessful()).thenReturn(false);
when(response.body()).thenReturn(null);
when(mapper.writeValueAsString(report)).thenReturn("{payload}");
// when,then
assertThatThrownBy(() -> underTest.uploadReport("project", "repository", "commit", report))
.isInstanceOf(BitbucketException.class);
}
@Test
public void testUploadReportFailsWithMessage() throws IOException {
// given
ErrorResponse.Error error = new ErrorResponse.Error("error!");
ErrorResponse errorResponse = new ErrorResponse(Sets.newHashSet(error));
CodeInsightsReport report = mock(CodeInsightsReport.class);
Call call = mock(Call.class);
Response response = mock(Response.class);
ResponseBody responseBody = mock(ResponseBody.class);
ObjectReader reader = mock(ObjectReader.class);
when(client.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(response);
when(response.isSuccessful()).thenReturn(false);
when(response.body()).thenReturn(responseBody);
when(responseBody.string()).thenReturn("error!");
when(response.code()).thenReturn(400);
when(mapper.writeValueAsString(report)).thenReturn("{payload}");
when(mapper.reader()).thenReturn(reader);
when(reader.forType(ErrorResponse.class)).thenReturn(reader);
when(reader.readValue(any(String.class))).thenReturn(errorResponse);
// when,then
assertThatThrownBy(() -> underTest.uploadReport("project", "repository", "commit", report))
.isInstanceOf(BitbucketException.class)
.hasMessage("error!")
.extracting(e -> ((BitbucketException) e).isError(400))
.isEqualTo(true);
}
@Test
public void testUploadAnnotations() throws IOException {
// given
CodeInsightsAnnotation annotation = mock(Annotation.class);
Set<CodeInsightsAnnotation> annotations = Sets.newHashSet(annotation);
Call call = mock(Call.class);
Response response = mock(Response.class);
ArgumentCaptor<Request> captor = ArgumentCaptor.forClass(Request.class);
when(client.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(response);
when(response.isSuccessful()).thenReturn(true);
when(mapper.writeValueAsString(any())).thenReturn("{payload}");
// when
underTest.uploadAnnotations("project", "repository", "commit", annotations);
// then
verify(client, times(1)).newCall(captor.capture());
Request request = captor.getValue();
assertEquals("POST", request.method());
assertEquals("https://my-server.org/rest/insights/1.0/projects/project/repos/repository/commits/commit/reports/com.github.mc1arke.sonarqube/annotations", request.url().toString());
}
@Test
public void testUploadAnnotationsWithEmptyAnnotations() throws IOException {
// given
Set<CodeInsightsAnnotation> annotations = Sets.newHashSet();
// when
underTest.uploadAnnotations("project", "repository", "commit", annotations);
// then
verify(client, times(0)).newCall(any());
}
@Test
public void testDeleteAnnotations() throws IOException {
// given
Call call = mock(Call.class);
Response response = mock(Response.class);
ArgumentCaptor<Request> captor = ArgumentCaptor.forClass(Request.class);
when(client.newCall(any())).thenReturn(call);
when(call.execute()).thenReturn(response);
when(response.isSuccessful()).thenReturn(true);
// when
underTest.deleteAnnotations("project", "repository", "commit");
// then
verify(client, times(1)).newCall(captor.capture());
Request request = captor.getValue();
assertEquals("DELETE", request.method());
assertEquals("https://my-server.org/rest/insights/1.0/projects/project/repos/repository/commits/commit/reports/com.github.mc1arke.sonarqube/annotations", request.url().toString());
}
@Test
public void testCreateAnnotationForServer() {
// given
// when
CodeInsightsAnnotation annotation = underTest.createCodeInsightsAnnotation("issueKey", 12, "http://localhost:9000/dashboard", "Failed", "/path/to/file", "MAJOR", "BUG");
// then
assertTrue(annotation instanceof Annotation);
assertEquals("issueKey", ((Annotation) annotation).getExternalId());
assertEquals(12, ((Annotation) annotation).getLine());
assertEquals("http://localhost:9000/dashboard", ((Annotation) annotation).getLink());
assertEquals("/path/to/file", ((Annotation) annotation).getPath());
assertEquals("MAJOR", ((Annotation) annotation).getSeverity());
assertEquals("BUG", ((Annotation) annotation).getType());
}
@Test
public void testCreateDataLinkForServer() {
// given
// when
DataValue data = underTest.createLinkDataValue("https://localhost:9000/any/project");
// then
assertTrue(data instanceof DataValue.Link);
assertEquals("https://localhost:9000/any/project", ((DataValue.Link) data).getHref());
}
@Test
public void testCreateCloudReport() {
// given
// when
CodeInsightsReport result = underTest.createCodeInsightsReport(new ArrayList<>(), "reportDescription", Instant.now(), "dashboardUrl", "logoUrl", QualityGate.Status.ERROR);
// then
assertTrue(result instanceof CreateReportRequest);
assertEquals(0, ((CreateReportRequest) result).getData().size());
assertEquals("reportDescription", ((CreateReportRequest) result).getDetails());
assertEquals("dashboardUrl", ((CreateReportRequest) result).getLink());
assertEquals("logoUrl", ((CreateReportRequest) result).getLogoUrl());
assertEquals("FAIL", ((CreateReportRequest) result).getResult());
}
}

View File

@ -0,0 +1,53 @@
package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(MockitoJUnitRunner.class)
public class BitbucketConfigurationUnitTest {
@Test
public void testIsCloudTrue() {
// given
BitbucketConfiguration configuration = new BitbucketConfiguration("https://api.bitbucket.org", "token", "repository", "project");
// when
boolean result = configuration.isCloud();
// then
assertTrue(result);
assertEquals("token", configuration.getToken());
assertEquals("repository", configuration.getRepository());
assertEquals("https://api.bitbucket.org", configuration.getUrl());
assertEquals("project", configuration.getProject());
}
@Test
public void testIsCloudTrueForOtherCasing() {
// given
BitbucketConfiguration configuration = new BitbucketConfiguration("https://API.BITBUCKET.org", "token", "repository", "project");
// when
boolean result = configuration.isCloud();
// then
assertTrue(result);
}
@Test
public void testIsCloudReturnsFalseForServerVersion() {
// given
BitbucketConfiguration configuration = new BitbucketConfiguration("https://API.server.org", "token", "repository", "project");
// when
boolean result = configuration.isCloud();
// then
assertFalse(result);
}
}

View File

@ -191,7 +191,7 @@ public class CommunityBranchSupportDelegateTest {
when(componentKey.getBranch()).thenReturn(Optional.of(new BranchSupport.Branch("dummy", BranchType.BRANCH)));
when(componentKey.getPullRequestKey()).thenReturn(Optional.empty());
ComponentDao componentDao = spy(mock(ComponentDao.class));
ComponentDao componentDao = mock(ComponentDao.class);
DbClient dbClient = mock(DbClient.class);
when(dbClient.componentDao()).thenReturn(componentDao);
@ -273,7 +273,7 @@ public class CommunityBranchSupportDelegateTest {
when(componentDto.getKey()).thenReturn("componentKey");
when(componentDto.uuid()).thenReturn("componentUuid");
ComponentDto copyComponentDto = spy(ComponentDto.class);
ComponentDto copyComponentDto = mock(ComponentDto.class);
when(componentDto.copy()).thenReturn(copyComponentDto);
BranchDto branchDto = mock(BranchDto.class);
@ -290,7 +290,7 @@ public class CommunityBranchSupportDelegateTest {
when(componentKey.getBranch()).thenReturn(Optional.empty());
when(componentKey.getPullRequestKey()).thenReturn(Optional.empty());
ComponentDao componentDao = spy(mock(ComponentDao.class));
ComponentDao componentDao = mock(ComponentDao.class);
DbClient dbClient = mock(DbClient.class);
when(dbClient.componentDao()).thenReturn(componentDao);

View File

@ -0,0 +1 @@
mock-maker-inline